@yuaone/core 0.1.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 +663 -0
- package/README.md +15 -0
- package/dist/__tests__/context-manager.test.d.ts +6 -0
- package/dist/__tests__/context-manager.test.d.ts.map +1 -0
- package/dist/__tests__/context-manager.test.js +220 -0
- package/dist/__tests__/context-manager.test.js.map +1 -0
- package/dist/__tests__/governor.test.d.ts +6 -0
- package/dist/__tests__/governor.test.d.ts.map +1 -0
- package/dist/__tests__/governor.test.js +210 -0
- package/dist/__tests__/governor.test.js.map +1 -0
- package/dist/__tests__/model-router.test.d.ts +6 -0
- package/dist/__tests__/model-router.test.d.ts.map +1 -0
- package/dist/__tests__/model-router.test.js +329 -0
- package/dist/__tests__/model-router.test.js.map +1 -0
- package/dist/agent-logger.d.ts +384 -0
- package/dist/agent-logger.d.ts.map +1 -0
- package/dist/agent-logger.js +820 -0
- package/dist/agent-logger.js.map +1 -0
- package/dist/agent-loop.d.ts +163 -0
- package/dist/agent-loop.d.ts.map +1 -0
- package/dist/agent-loop.js +609 -0
- package/dist/agent-loop.js.map +1 -0
- package/dist/agent-modes.d.ts +85 -0
- package/dist/agent-modes.d.ts.map +1 -0
- package/dist/agent-modes.js +418 -0
- package/dist/agent-modes.js.map +1 -0
- package/dist/approval.d.ts +137 -0
- package/dist/approval.d.ts.map +1 -0
- package/dist/approval.js +299 -0
- package/dist/approval.js.map +1 -0
- package/dist/async-completion-queue.d.ts +56 -0
- package/dist/async-completion-queue.d.ts.map +1 -0
- package/dist/async-completion-queue.js +77 -0
- package/dist/async-completion-queue.js.map +1 -0
- package/dist/auto-fix.d.ts +174 -0
- package/dist/auto-fix.d.ts.map +1 -0
- package/dist/auto-fix.js +319 -0
- package/dist/auto-fix.js.map +1 -0
- package/dist/codebase-context.d.ts +396 -0
- package/dist/codebase-context.d.ts.map +1 -0
- package/dist/codebase-context.js +1260 -0
- package/dist/codebase-context.js.map +1 -0
- package/dist/conflict-resolver.d.ts +191 -0
- package/dist/conflict-resolver.d.ts.map +1 -0
- package/dist/conflict-resolver.js +524 -0
- package/dist/conflict-resolver.js.map +1 -0
- package/dist/constants.d.ts +52 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +141 -0
- package/dist/constants.js.map +1 -0
- package/dist/context-budget.d.ts +435 -0
- package/dist/context-budget.d.ts.map +1 -0
- package/dist/context-budget.js +903 -0
- package/dist/context-budget.js.map +1 -0
- package/dist/context-compressor.d.ts +143 -0
- package/dist/context-compressor.d.ts.map +1 -0
- package/dist/context-compressor.js +511 -0
- package/dist/context-compressor.js.map +1 -0
- package/dist/context-manager.d.ts +112 -0
- package/dist/context-manager.d.ts.map +1 -0
- package/dist/context-manager.js +247 -0
- package/dist/context-manager.js.map +1 -0
- package/dist/continuous-reflection.d.ts +267 -0
- package/dist/continuous-reflection.d.ts.map +1 -0
- package/dist/continuous-reflection.js +338 -0
- package/dist/continuous-reflection.js.map +1 -0
- package/dist/cross-file-refactor.d.ts +352 -0
- package/dist/cross-file-refactor.d.ts.map +1 -0
- package/dist/cross-file-refactor.js +1544 -0
- package/dist/cross-file-refactor.js.map +1 -0
- package/dist/dag-orchestrator.d.ts +138 -0
- package/dist/dag-orchestrator.d.ts.map +1 -0
- package/dist/dag-orchestrator.js +379 -0
- package/dist/dag-orchestrator.js.map +1 -0
- package/dist/debate-orchestrator.d.ts +301 -0
- package/dist/debate-orchestrator.d.ts.map +1 -0
- package/dist/debate-orchestrator.js +719 -0
- package/dist/debate-orchestrator.js.map +1 -0
- package/dist/dependency-analyzer.d.ts +113 -0
- package/dist/dependency-analyzer.d.ts.map +1 -0
- package/dist/dependency-analyzer.js +444 -0
- package/dist/dependency-analyzer.js.map +1 -0
- package/dist/design-loop.d.ts +59 -0
- package/dist/design-loop.d.ts.map +1 -0
- package/dist/design-loop.js +344 -0
- package/dist/design-loop.js.map +1 -0
- package/dist/doc-intelligence.d.ts +383 -0
- package/dist/doc-intelligence.d.ts.map +1 -0
- package/dist/doc-intelligence.js +1307 -0
- package/dist/doc-intelligence.js.map +1 -0
- package/dist/dynamic-role-generator.d.ts +76 -0
- package/dist/dynamic-role-generator.d.ts.map +1 -0
- package/dist/dynamic-role-generator.js +194 -0
- package/dist/dynamic-role-generator.js.map +1 -0
- package/dist/errors.d.ts +69 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +102 -0
- package/dist/errors.js.map +1 -0
- package/dist/event-bus.d.ts +159 -0
- package/dist/event-bus.d.ts.map +1 -0
- package/dist/event-bus.js +305 -0
- package/dist/event-bus.js.map +1 -0
- package/dist/execution-engine.d.ts +425 -0
- package/dist/execution-engine.d.ts.map +1 -0
- package/dist/execution-engine.js +1555 -0
- package/dist/execution-engine.js.map +1 -0
- package/dist/git-intelligence.d.ts +306 -0
- package/dist/git-intelligence.d.ts.map +1 -0
- package/dist/git-intelligence.js +1099 -0
- package/dist/git-intelligence.js.map +1 -0
- package/dist/governor.d.ts +77 -0
- package/dist/governor.d.ts.map +1 -0
- package/dist/governor.js +161 -0
- package/dist/governor.js.map +1 -0
- package/dist/hierarchical-planner.d.ts +313 -0
- package/dist/hierarchical-planner.d.ts.map +1 -0
- package/dist/hierarchical-planner.js +981 -0
- package/dist/hierarchical-planner.js.map +1 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +123 -0
- package/dist/index.js.map +1 -0
- package/dist/intent-inference.d.ts +103 -0
- package/dist/intent-inference.d.ts.map +1 -0
- package/dist/intent-inference.js +605 -0
- package/dist/intent-inference.js.map +1 -0
- package/dist/interrupt-manager.d.ts +143 -0
- package/dist/interrupt-manager.d.ts.map +1 -0
- package/dist/interrupt-manager.js +196 -0
- package/dist/interrupt-manager.js.map +1 -0
- package/dist/kernel.d.ts +564 -0
- package/dist/kernel.d.ts.map +1 -0
- package/dist/kernel.js +1419 -0
- package/dist/kernel.js.map +1 -0
- package/dist/language-support.d.ts +232 -0
- package/dist/language-support.d.ts.map +1 -0
- package/dist/language-support.js +1134 -0
- package/dist/language-support.js.map +1 -0
- package/dist/llm-client.d.ts +82 -0
- package/dist/llm-client.d.ts.map +1 -0
- package/dist/llm-client.js +475 -0
- package/dist/llm-client.js.map +1 -0
- package/dist/mcp-client.d.ts +232 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +718 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/memory-manager.d.ts +200 -0
- package/dist/memory-manager.d.ts.map +1 -0
- package/dist/memory-manager.js +568 -0
- package/dist/memory-manager.js.map +1 -0
- package/dist/memory.d.ts +87 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +341 -0
- package/dist/memory.js.map +1 -0
- package/dist/model-router.d.ts +245 -0
- package/dist/model-router.d.ts.map +1 -0
- package/dist/model-router.js +632 -0
- package/dist/model-router.js.map +1 -0
- package/dist/parallel-executor.d.ts +125 -0
- package/dist/parallel-executor.d.ts.map +1 -0
- package/dist/parallel-executor.js +201 -0
- package/dist/parallel-executor.js.map +1 -0
- package/dist/perf-optimizer.d.ts +212 -0
- package/dist/perf-optimizer.d.ts.map +1 -0
- package/dist/perf-optimizer.js +721 -0
- package/dist/perf-optimizer.js.map +1 -0
- package/dist/persona.d.ts +305 -0
- package/dist/persona.d.ts.map +1 -0
- package/dist/persona.js +887 -0
- package/dist/persona.js.map +1 -0
- package/dist/planner.d.ts +70 -0
- package/dist/planner.d.ts.map +1 -0
- package/dist/planner.js +264 -0
- package/dist/planner.js.map +1 -0
- package/dist/qa-pipeline.d.ts +365 -0
- package/dist/qa-pipeline.d.ts.map +1 -0
- package/dist/qa-pipeline.js +1352 -0
- package/dist/qa-pipeline.js.map +1 -0
- package/dist/reasoning-adapter.d.ts +116 -0
- package/dist/reasoning-adapter.d.ts.map +1 -0
- package/dist/reasoning-adapter.js +187 -0
- package/dist/reasoning-adapter.js.map +1 -0
- package/dist/role-registry.d.ts +55 -0
- package/dist/role-registry.d.ts.map +1 -0
- package/dist/role-registry.js +192 -0
- package/dist/role-registry.js.map +1 -0
- package/dist/sandbox-tiers.d.ts +327 -0
- package/dist/sandbox-tiers.d.ts.map +1 -0
- package/dist/sandbox-tiers.js +928 -0
- package/dist/sandbox-tiers.js.map +1 -0
- package/dist/security-scanner.d.ts +222 -0
- package/dist/security-scanner.d.ts.map +1 -0
- package/dist/security-scanner.js +1129 -0
- package/dist/security-scanner.js.map +1 -0
- package/dist/security.d.ts +93 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +393 -0
- package/dist/security.js.map +1 -0
- package/dist/self-reflection.d.ts +397 -0
- package/dist/self-reflection.d.ts.map +1 -0
- package/dist/self-reflection.js +908 -0
- package/dist/self-reflection.js.map +1 -0
- package/dist/session-persistence.d.ts +191 -0
- package/dist/session-persistence.d.ts.map +1 -0
- package/dist/session-persistence.js +395 -0
- package/dist/session-persistence.js.map +1 -0
- package/dist/speculative-executor.d.ts +210 -0
- package/dist/speculative-executor.d.ts.map +1 -0
- package/dist/speculative-executor.js +618 -0
- package/dist/speculative-executor.js.map +1 -0
- package/dist/state-machine.d.ts +289 -0
- package/dist/state-machine.d.ts.map +1 -0
- package/dist/state-machine.js +695 -0
- package/dist/state-machine.js.map +1 -0
- package/dist/sub-agent.d.ts +177 -0
- package/dist/sub-agent.d.ts.map +1 -0
- package/dist/sub-agent.js +303 -0
- package/dist/sub-agent.js.map +1 -0
- package/dist/system-prompt.d.ts +26 -0
- package/dist/system-prompt.d.ts.map +1 -0
- package/dist/system-prompt.js +84 -0
- package/dist/system-prompt.js.map +1 -0
- package/dist/test-intelligence.d.ts +439 -0
- package/dist/test-intelligence.d.ts.map +1 -0
- package/dist/test-intelligence.js +1165 -0
- package/dist/test-intelligence.js.map +1 -0
- package/dist/types.d.ts +632 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/vector-index.d.ts +314 -0
- package/dist/vector-index.d.ts.map +1 -0
- package/dist/vector-index.js +618 -0
- package/dist/vector-index.js.map +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,981 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module hierarchical-planner
|
|
3
|
+
* @description 3-Level Hierarchical Planner — Strategic / Tactical / Operational.
|
|
4
|
+
*
|
|
5
|
+
* 사용자의 고수준 목표를 3단계 계획으로 분해한다:
|
|
6
|
+
* - L1 Strategic: 목표 → 서브골 분해 (Flagship 모델)
|
|
7
|
+
* - L2 Tactical: 서브골 → 파일별 태스크 (Premium 모델)
|
|
8
|
+
* - L3 Operational: 태스크 → 구체적 도구 호출 (Standard 모델)
|
|
9
|
+
*
|
|
10
|
+
* 기존 Planner(단일 레벨)와 호환되는 toExecutionPlan() 변환을 지원한다.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const planner = new HierarchicalPlanner({ projectPath: "/app" });
|
|
15
|
+
* const plan = await planner.createHierarchicalPlan("Add auth to the API", llmClient);
|
|
16
|
+
* const execPlan = planner.toExecutionPlan(plan);
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
20
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
// ─── Internal Defaults ───
|
|
23
|
+
const DEFAULTS = {
|
|
24
|
+
strategicModel: "flagship",
|
|
25
|
+
tacticalModel: "premium",
|
|
26
|
+
operationalModel: "standard",
|
|
27
|
+
maxSubGoals: 10,
|
|
28
|
+
maxTasksPerGoal: 20,
|
|
29
|
+
maxActionsPerTask: 50,
|
|
30
|
+
enableOperationalPrePlanning: false,
|
|
31
|
+
};
|
|
32
|
+
/** Tokens per iteration estimate for budget calculation */
|
|
33
|
+
const TOKENS_PER_ITERATION = 3_000;
|
|
34
|
+
/** High-risk file patterns that trigger elevated risk assessment */
|
|
35
|
+
const HIGH_RISK_PATTERNS = [
|
|
36
|
+
/\.env/i,
|
|
37
|
+
/secret/i,
|
|
38
|
+
/credential/i,
|
|
39
|
+
/auth/i,
|
|
40
|
+
/password/i,
|
|
41
|
+
/token/i,
|
|
42
|
+
/config\.(ts|js|json)$/i,
|
|
43
|
+
/package\.json$/,
|
|
44
|
+
/tsconfig/i,
|
|
45
|
+
/docker/i,
|
|
46
|
+
/\.ya?ml$/i,
|
|
47
|
+
/migration/i,
|
|
48
|
+
/schema\.(prisma|sql)$/i,
|
|
49
|
+
];
|
|
50
|
+
/** Destructive operations that elevate risk */
|
|
51
|
+
const DESTRUCTIVE_OPS = [
|
|
52
|
+
"file_write",
|
|
53
|
+
"shell_exec",
|
|
54
|
+
"git_ops",
|
|
55
|
+
];
|
|
56
|
+
// ─── HierarchicalPlanner ───
|
|
57
|
+
/**
|
|
58
|
+
* HierarchicalPlanner — 3-level planning hierarchy for the YUAN coding agent.
|
|
59
|
+
*
|
|
60
|
+
* Decomposes a high-level user goal into:
|
|
61
|
+
* 1. **Strategic** — what sub-goals to pursue, risk assessment, capability analysis
|
|
62
|
+
* 2. **Tactical** — which files to touch, which tools to use, dependency ordering
|
|
63
|
+
* 3. **Operational** — exact tool calls with pre-planned arguments
|
|
64
|
+
*
|
|
65
|
+
* Supports re-planning when execution encounters errors or new information.
|
|
66
|
+
* Backward-compatible with the existing `ExecutionPlan` format via `toExecutionPlan()`.
|
|
67
|
+
*/
|
|
68
|
+
export class HierarchicalPlanner {
|
|
69
|
+
config;
|
|
70
|
+
projectContext;
|
|
71
|
+
constructor(config) {
|
|
72
|
+
this.config = {
|
|
73
|
+
projectPath: config.projectPath,
|
|
74
|
+
strategicModel: config.strategicModel ?? DEFAULTS.strategicModel,
|
|
75
|
+
tacticalModel: config.tacticalModel ?? DEFAULTS.tacticalModel,
|
|
76
|
+
operationalModel: config.operationalModel ?? DEFAULTS.operationalModel,
|
|
77
|
+
maxSubGoals: config.maxSubGoals ?? DEFAULTS.maxSubGoals,
|
|
78
|
+
maxTasksPerGoal: config.maxTasksPerGoal ?? DEFAULTS.maxTasksPerGoal,
|
|
79
|
+
maxActionsPerTask: config.maxActionsPerTask ?? DEFAULTS.maxActionsPerTask,
|
|
80
|
+
enableOperationalPrePlanning: config.enableOperationalPrePlanning ?? DEFAULTS.enableOperationalPrePlanning,
|
|
81
|
+
};
|
|
82
|
+
this.projectContext = "";
|
|
83
|
+
}
|
|
84
|
+
// ─── Public API ───
|
|
85
|
+
/**
|
|
86
|
+
* L1: Strategic Planning — decompose a high-level goal into sub-goals.
|
|
87
|
+
* Uses a flagship model for best reasoning quality.
|
|
88
|
+
*
|
|
89
|
+
* @param goal - The user's high-level goal
|
|
90
|
+
* @param llmClient - BYOK LLM client for the strategic model
|
|
91
|
+
* @param projectContext - Optional pre-gathered project context
|
|
92
|
+
* @returns Strategic goal with sub-goals, complexity, and risk assessment
|
|
93
|
+
*/
|
|
94
|
+
async planStrategic(goal, llmClient, projectContext) {
|
|
95
|
+
const ctx = projectContext ?? await this.gatherProjectContext();
|
|
96
|
+
this.projectContext = ctx;
|
|
97
|
+
const prompt = this.buildStrategicPrompt(goal, ctx);
|
|
98
|
+
const messages = [
|
|
99
|
+
{ role: "system", content: prompt },
|
|
100
|
+
{ role: "user", content: goal },
|
|
101
|
+
];
|
|
102
|
+
const response = await llmClient.chat(messages);
|
|
103
|
+
const parsed = this.parseStrategicResponse(response.content ?? "", goal);
|
|
104
|
+
// Enforce sub-goal limit
|
|
105
|
+
if (parsed.subGoals.length > this.config.maxSubGoals) {
|
|
106
|
+
parsed.subGoals = parsed.subGoals.slice(0, this.config.maxSubGoals);
|
|
107
|
+
}
|
|
108
|
+
// Enrich with risk assessment if not provided by LLM
|
|
109
|
+
if (!parsed.riskAssessment || !parsed.riskAssessment.level) {
|
|
110
|
+
const allFiles = parsed.subGoals.flatMap((sg) => sg.targetFiles);
|
|
111
|
+
parsed.riskAssessment = this.assessRisk(goal, allFiles);
|
|
112
|
+
}
|
|
113
|
+
return parsed;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* L2: Tactical Planning — plan file-level tasks for each sub-goal.
|
|
117
|
+
* Uses a premium model for good coding analysis.
|
|
118
|
+
*
|
|
119
|
+
* @param strategicGoal - The strategic goal to decompose
|
|
120
|
+
* @param llmClient - BYOK LLM client for the tactical model
|
|
121
|
+
* @param fileContext - Optional map of file path → content for context
|
|
122
|
+
* @returns Array of tactical tasks with dependencies and ordering
|
|
123
|
+
*/
|
|
124
|
+
async planTactical(strategicGoal, llmClient, fileContext) {
|
|
125
|
+
const allTasks = [];
|
|
126
|
+
for (const subGoal of strategicGoal.subGoals) {
|
|
127
|
+
const prompt = this.buildTacticalPrompt(subGoal, fileContext);
|
|
128
|
+
const messages = [
|
|
129
|
+
{ role: "system", content: prompt },
|
|
130
|
+
{
|
|
131
|
+
role: "user",
|
|
132
|
+
content: `Plan implementation for: ${subGoal.description}\nTarget files: ${subGoal.targetFiles.join(", ")}`,
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
const response = await llmClient.chat(messages);
|
|
136
|
+
const tasks = this.parseTacticalResponse(response.content ?? "", strategicGoal.id, subGoal);
|
|
137
|
+
// Enforce task limit per goal
|
|
138
|
+
const limited = tasks.slice(0, this.config.maxTasksPerGoal);
|
|
139
|
+
allTasks.push(...limited);
|
|
140
|
+
}
|
|
141
|
+
// Assign global ordering based on dependencies
|
|
142
|
+
return this.buildDependencyChain(allTasks);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* L3: Operational Planning — plan exact tool calls for a task.
|
|
146
|
+
* Uses a standard model for cost efficiency on detailed step planning.
|
|
147
|
+
*
|
|
148
|
+
* @param task - The tactical task to detail
|
|
149
|
+
* @param llmClient - BYOK LLM client for the operational model
|
|
150
|
+
* @param currentState - Optional current execution state for context
|
|
151
|
+
* @returns Array of operational actions with tool inputs
|
|
152
|
+
*/
|
|
153
|
+
async planOperational(task, llmClient, currentState) {
|
|
154
|
+
const prompt = this.buildOperationalPrompt(task, currentState);
|
|
155
|
+
const messages = [
|
|
156
|
+
{ role: "system", content: prompt },
|
|
157
|
+
{
|
|
158
|
+
role: "user",
|
|
159
|
+
content: `Plan exact actions for: ${task.description}\nTools: ${task.toolStrategy.join(", ")}\nFiles: ${task.targetFiles.join(", ")}`,
|
|
160
|
+
},
|
|
161
|
+
];
|
|
162
|
+
const response = await llmClient.chat(messages);
|
|
163
|
+
const actions = this.parseOperationalResponse(response.content ?? "", task.id);
|
|
164
|
+
// Enforce action limit
|
|
165
|
+
return actions.slice(0, this.config.maxActionsPerTask);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Create a full hierarchical plan from a user goal.
|
|
169
|
+
* Runs L1 → L2 sequentially; L3 is conditional on config.
|
|
170
|
+
*
|
|
171
|
+
* @param goal - The user's high-level goal
|
|
172
|
+
* @param llmClient - BYOK LLM client (used for all levels)
|
|
173
|
+
* @returns Complete hierarchical plan
|
|
174
|
+
*/
|
|
175
|
+
async createHierarchicalPlan(goal, llmClient) {
|
|
176
|
+
// L1: Strategic
|
|
177
|
+
const strategic = await this.planStrategic(goal, llmClient);
|
|
178
|
+
// L2: Tactical
|
|
179
|
+
const tactical = await this.planTactical(strategic, llmClient);
|
|
180
|
+
// L3: Operational (optional pre-planning)
|
|
181
|
+
const operational = new Map();
|
|
182
|
+
if (this.config.enableOperationalPrePlanning) {
|
|
183
|
+
for (const task of tactical) {
|
|
184
|
+
const actions = await this.planOperational(task, llmClient);
|
|
185
|
+
operational.set(task.id, actions);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Compute metadata
|
|
189
|
+
const parallelizableGroups = this.findParallelGroups(tactical);
|
|
190
|
+
const criticalPath = this.findCriticalPath(tactical);
|
|
191
|
+
const totalEstimatedIterations = tactical.reduce((sum, t) => sum + t.estimatedIterations, 0);
|
|
192
|
+
const plan = {
|
|
193
|
+
id: randomUUID(),
|
|
194
|
+
goal,
|
|
195
|
+
createdAt: Date.now(),
|
|
196
|
+
strategic,
|
|
197
|
+
tactical,
|
|
198
|
+
operational,
|
|
199
|
+
totalEstimatedIterations,
|
|
200
|
+
parallelizableGroups,
|
|
201
|
+
criticalPath,
|
|
202
|
+
estimatedTokenBudget: 0,
|
|
203
|
+
};
|
|
204
|
+
plan.estimatedTokenBudget = this.estimateTokenBudget(plan);
|
|
205
|
+
return plan;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Re-plan after encountering an error, new information, or other trigger.
|
|
209
|
+
* Decides the best recovery strategy and returns modified tasks.
|
|
210
|
+
*
|
|
211
|
+
* @param currentPlan - The current hierarchical plan
|
|
212
|
+
* @param trigger - What triggered the re-plan
|
|
213
|
+
* @param llmClient - BYOK LLM client for re-planning
|
|
214
|
+
* @returns Re-plan result with strategy and modified tasks
|
|
215
|
+
*/
|
|
216
|
+
async replan(currentPlan, trigger, llmClient) {
|
|
217
|
+
const prompt = this.buildReplanPrompt(currentPlan, trigger);
|
|
218
|
+
const messages = [
|
|
219
|
+
{ role: "system", content: prompt },
|
|
220
|
+
{
|
|
221
|
+
role: "user",
|
|
222
|
+
content: `Re-plan needed: ${trigger.description}\nType: ${trigger.type}\nSeverity: ${trigger.severity}\nAffected tasks: ${trigger.affectedTaskIds.join(", ")}`,
|
|
223
|
+
},
|
|
224
|
+
];
|
|
225
|
+
const response = await llmClient.chat(messages);
|
|
226
|
+
return this.parseReplanResponse(response.content ?? "", currentPlan, trigger);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Convert a HierarchicalPlan to the existing ExecutionPlan format.
|
|
230
|
+
* Provides backward compatibility with code that uses the single-level Planner.
|
|
231
|
+
*
|
|
232
|
+
* @param plan - Hierarchical plan to convert
|
|
233
|
+
* @returns ExecutionPlan compatible with existing agent infrastructure
|
|
234
|
+
*/
|
|
235
|
+
toExecutionPlan(plan) {
|
|
236
|
+
const steps = plan.tactical.map((task) => ({
|
|
237
|
+
id: task.id,
|
|
238
|
+
goal: task.description,
|
|
239
|
+
targetFiles: task.targetFiles,
|
|
240
|
+
readFiles: task.readFiles,
|
|
241
|
+
tools: task.toolStrategy,
|
|
242
|
+
estimatedIterations: task.estimatedIterations,
|
|
243
|
+
dependsOn: task.dependsOn,
|
|
244
|
+
}));
|
|
245
|
+
return {
|
|
246
|
+
goal: plan.goal,
|
|
247
|
+
steps,
|
|
248
|
+
estimatedTokens: plan.estimatedTokenBudget,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Find groups of tasks that can execute in parallel.
|
|
253
|
+
* Tasks with no mutual dependencies can run concurrently.
|
|
254
|
+
*
|
|
255
|
+
* @param tasks - Tactical tasks to analyze
|
|
256
|
+
* @returns Array of groups, each group is an array of task IDs
|
|
257
|
+
*/
|
|
258
|
+
findParallelGroups(tasks) {
|
|
259
|
+
if (tasks.length === 0)
|
|
260
|
+
return [];
|
|
261
|
+
const taskMap = new Map(tasks.map((t) => [t.id, t]));
|
|
262
|
+
const completed = new Set();
|
|
263
|
+
const groups = [];
|
|
264
|
+
// Iterative topological grouping
|
|
265
|
+
let remaining = tasks.length;
|
|
266
|
+
const maxIterations = tasks.length + 1; // safety bound
|
|
267
|
+
let iteration = 0;
|
|
268
|
+
while (remaining > 0 && iteration < maxIterations) {
|
|
269
|
+
iteration++;
|
|
270
|
+
const group = [];
|
|
271
|
+
for (const task of tasks) {
|
|
272
|
+
if (completed.has(task.id))
|
|
273
|
+
continue;
|
|
274
|
+
// Check if all dependencies are completed
|
|
275
|
+
const depsReady = task.dependsOn.every((dep) => completed.has(dep) || !taskMap.has(dep));
|
|
276
|
+
if (depsReady) {
|
|
277
|
+
group.push(task.id);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (group.length === 0) {
|
|
281
|
+
// Circular dependency detected — break remaining tasks into single group
|
|
282
|
+
const stuck = tasks
|
|
283
|
+
.filter((t) => !completed.has(t.id))
|
|
284
|
+
.map((t) => t.id);
|
|
285
|
+
groups.push(stuck);
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
groups.push(group);
|
|
289
|
+
for (const id of group) {
|
|
290
|
+
completed.add(id);
|
|
291
|
+
remaining--;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return groups;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Find the critical path — the longest sequential dependency chain.
|
|
298
|
+
* Uses DFS with memoization for path length calculation.
|
|
299
|
+
*
|
|
300
|
+
* @param tasks - Tactical tasks to analyze
|
|
301
|
+
* @returns Array of task IDs forming the critical path
|
|
302
|
+
*/
|
|
303
|
+
findCriticalPath(tasks) {
|
|
304
|
+
if (tasks.length === 0)
|
|
305
|
+
return [];
|
|
306
|
+
const taskMap = new Map(tasks.map((t) => [t.id, t]));
|
|
307
|
+
const memo = new Map();
|
|
308
|
+
/**
|
|
309
|
+
* Recursively find the longest path starting from a task.
|
|
310
|
+
*/
|
|
311
|
+
const longestFrom = (taskId, visited) => {
|
|
312
|
+
if (memo.has(taskId))
|
|
313
|
+
return memo.get(taskId);
|
|
314
|
+
if (visited.has(taskId))
|
|
315
|
+
return [taskId]; // cycle — stop
|
|
316
|
+
visited.add(taskId);
|
|
317
|
+
const task = taskMap.get(taskId);
|
|
318
|
+
if (!task)
|
|
319
|
+
return [taskId];
|
|
320
|
+
// Find dependents (tasks that depend on this one)
|
|
321
|
+
const dependents = tasks.filter((t) => t.dependsOn.includes(taskId));
|
|
322
|
+
if (dependents.length === 0) {
|
|
323
|
+
const path = [taskId];
|
|
324
|
+
memo.set(taskId, path);
|
|
325
|
+
visited.delete(taskId);
|
|
326
|
+
return path;
|
|
327
|
+
}
|
|
328
|
+
let longestChild = [];
|
|
329
|
+
for (const dep of dependents) {
|
|
330
|
+
const childPath = longestFrom(dep.id, visited);
|
|
331
|
+
if (childPath.length > longestChild.length) {
|
|
332
|
+
longestChild = childPath;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const path = [taskId, ...longestChild];
|
|
336
|
+
memo.set(taskId, path);
|
|
337
|
+
visited.delete(taskId);
|
|
338
|
+
return path;
|
|
339
|
+
};
|
|
340
|
+
// Find roots (tasks with no dependencies or only external deps)
|
|
341
|
+
const roots = tasks.filter((t) => t.dependsOn.length === 0 || t.dependsOn.every((d) => !taskMap.has(d)));
|
|
342
|
+
let criticalPath = [];
|
|
343
|
+
for (const root of roots) {
|
|
344
|
+
const path = longestFrom(root.id, new Set());
|
|
345
|
+
if (path.length > criticalPath.length) {
|
|
346
|
+
criticalPath = path;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// If no roots found (all circular), just return all task IDs
|
|
350
|
+
if (criticalPath.length === 0 && tasks.length > 0) {
|
|
351
|
+
criticalPath = tasks.map((t) => t.id);
|
|
352
|
+
}
|
|
353
|
+
return criticalPath;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Estimate the total token budget for a hierarchical plan.
|
|
357
|
+
* Based on iteration count, planning overhead, and operational detail.
|
|
358
|
+
*
|
|
359
|
+
* @param plan - The hierarchical plan
|
|
360
|
+
* @returns Estimated token budget
|
|
361
|
+
*/
|
|
362
|
+
estimateTokenBudget(plan) {
|
|
363
|
+
// Base: iterations * tokens per iteration
|
|
364
|
+
const iterationTokens = plan.totalEstimatedIterations * TOKENS_PER_ITERATION;
|
|
365
|
+
// Planning overhead: ~2K tokens per tactical task for planning
|
|
366
|
+
const planningOverhead = plan.tactical.length * 2_000;
|
|
367
|
+
// Operational overhead: if pre-planned, count actions
|
|
368
|
+
let operationalTokens = 0;
|
|
369
|
+
if (plan.operational.size > 0) {
|
|
370
|
+
for (const [, actions] of plan.operational) {
|
|
371
|
+
operationalTokens += actions.length * 500;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// Strategic overhead: one-time cost for L1 planning
|
|
375
|
+
const strategicOverhead = 5_000;
|
|
376
|
+
return iterationTokens + planningOverhead + operationalTokens + strategicOverhead;
|
|
377
|
+
}
|
|
378
|
+
// ─── Private: Project Context ───
|
|
379
|
+
/**
|
|
380
|
+
* Gather project context by reading package.json and directory listing.
|
|
381
|
+
* @returns Concatenated project context string
|
|
382
|
+
*/
|
|
383
|
+
async gatherProjectContext() {
|
|
384
|
+
const parts = [];
|
|
385
|
+
// package.json
|
|
386
|
+
try {
|
|
387
|
+
const pkgContent = await readFile(join(this.config.projectPath, "package.json"), "utf-8");
|
|
388
|
+
parts.push(`package.json:\n${pkgContent}`);
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
// no package.json
|
|
392
|
+
}
|
|
393
|
+
// tsconfig.json
|
|
394
|
+
try {
|
|
395
|
+
const tsContent = await readFile(join(this.config.projectPath, "tsconfig.json"), "utf-8");
|
|
396
|
+
parts.push(`tsconfig.json:\n${tsContent}`);
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
// no tsconfig
|
|
400
|
+
}
|
|
401
|
+
// Directory listing (shallow)
|
|
402
|
+
try {
|
|
403
|
+
const entries = await readdir(this.config.projectPath, {
|
|
404
|
+
withFileTypes: true,
|
|
405
|
+
});
|
|
406
|
+
const filtered = entries
|
|
407
|
+
.filter((e) => !e.name.startsWith(".") &&
|
|
408
|
+
e.name !== "node_modules" &&
|
|
409
|
+
e.name !== "dist")
|
|
410
|
+
.map((e) => (e.isDirectory() ? `${e.name}/` : e.name));
|
|
411
|
+
parts.push(`Project files:\n${filtered.join("\n")}`);
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
// can't list
|
|
415
|
+
}
|
|
416
|
+
return parts.join("\n\n");
|
|
417
|
+
}
|
|
418
|
+
// ─── Private: Prompt Builders ───
|
|
419
|
+
/**
|
|
420
|
+
* Build the system prompt for L1 strategic planning.
|
|
421
|
+
* Instructs the LLM to decompose the goal into sub-goals with JSON output.
|
|
422
|
+
*/
|
|
423
|
+
buildStrategicPrompt(goal, context) {
|
|
424
|
+
return `You are a senior software architect. Decompose the user's goal into strategic sub-goals.
|
|
425
|
+
|
|
426
|
+
## Project Context
|
|
427
|
+
${context}
|
|
428
|
+
|
|
429
|
+
## Goal
|
|
430
|
+
${goal}
|
|
431
|
+
|
|
432
|
+
## Instructions
|
|
433
|
+
- Break the goal into 1-${this.config.maxSubGoals} sub-goals
|
|
434
|
+
- Each sub-goal should be independently achievable
|
|
435
|
+
- Identify required capabilities (tools needed)
|
|
436
|
+
- Assess overall complexity and risk
|
|
437
|
+
- Consider file dependencies and ordering
|
|
438
|
+
|
|
439
|
+
## Output Format
|
|
440
|
+
Respond with ONLY a JSON object (no markdown fences):
|
|
441
|
+
{
|
|
442
|
+
"description": "overall strategic description",
|
|
443
|
+
"subGoals": [
|
|
444
|
+
{
|
|
445
|
+
"description": "what this sub-goal achieves",
|
|
446
|
+
"targetFiles": ["files to modify"],
|
|
447
|
+
"readFiles": ["files to read for context"],
|
|
448
|
+
"toolStrategy": ["tool names"],
|
|
449
|
+
"dependsOn": [],
|
|
450
|
+
"estimatedIterations": 3,
|
|
451
|
+
"modelPreference": "premium"
|
|
452
|
+
}
|
|
453
|
+
],
|
|
454
|
+
"complexity": "moderate",
|
|
455
|
+
"requiredCapabilities": ["file_write", "shell_exec"],
|
|
456
|
+
"riskAssessment": {
|
|
457
|
+
"level": "low",
|
|
458
|
+
"factors": [],
|
|
459
|
+
"mitigations": [],
|
|
460
|
+
"requiresApproval": false
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
Available tools: file_read, file_write, file_edit, shell_exec, grep, glob, git_ops, test_run, security_scan
|
|
465
|
+
|
|
466
|
+
Complexity levels: trivial, simple, moderate, complex, massive
|
|
467
|
+
Risk levels: low, medium, high, critical`;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Build the system prompt for L2 tactical planning.
|
|
471
|
+
* Instructs the LLM to plan file-level tasks for a sub-goal.
|
|
472
|
+
*/
|
|
473
|
+
buildTacticalPrompt(subGoal, fileContext) {
|
|
474
|
+
let contextSection = "";
|
|
475
|
+
if (fileContext && fileContext.size > 0) {
|
|
476
|
+
const entries = [];
|
|
477
|
+
for (const [path, content] of fileContext) {
|
|
478
|
+
// Truncate large files
|
|
479
|
+
const truncated = content.length > 2_000
|
|
480
|
+
? content.slice(0, 2_000) + "\n... (truncated)"
|
|
481
|
+
: content;
|
|
482
|
+
entries.push(`--- ${path} ---\n${truncated}`);
|
|
483
|
+
}
|
|
484
|
+
contextSection = `\n## File Context\n${entries.join("\n\n")}`;
|
|
485
|
+
}
|
|
486
|
+
return `You are a tech lead planning implementation tasks. For this sub-goal, plan specific file-level tasks.
|
|
487
|
+
|
|
488
|
+
## Sub-Goal
|
|
489
|
+
${subGoal.description}
|
|
490
|
+
|
|
491
|
+
## Target Files
|
|
492
|
+
${subGoal.targetFiles.join(", ") || "TBD"}
|
|
493
|
+
${contextSection}
|
|
494
|
+
|
|
495
|
+
## Instructions
|
|
496
|
+
- Plan concrete tasks that modify specific files
|
|
497
|
+
- Identify read-only dependencies (files to read but not modify)
|
|
498
|
+
- Choose the right tools for each task
|
|
499
|
+
- Estimate iterations conservatively (2-10 per file)
|
|
500
|
+
- Mark dependencies between tasks (by task ID)
|
|
501
|
+
- Suggest model preference: "flagship" for complex logic, "premium" for standard coding, "standard" for simple edits
|
|
502
|
+
|
|
503
|
+
## Output Format
|
|
504
|
+
Respond with ONLY a JSON object (no markdown fences):
|
|
505
|
+
{
|
|
506
|
+
"tasks": [
|
|
507
|
+
{
|
|
508
|
+
"description": "what this task does",
|
|
509
|
+
"targetFiles": ["src/foo.ts"],
|
|
510
|
+
"readFiles": ["src/types.ts"],
|
|
511
|
+
"toolStrategy": ["file_read", "file_edit"],
|
|
512
|
+
"dependsOn": [],
|
|
513
|
+
"estimatedIterations": 3,
|
|
514
|
+
"modelPreference": "premium"
|
|
515
|
+
}
|
|
516
|
+
]
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
Available tools: file_read, file_write, file_edit, shell_exec, grep, glob, git_ops, test_run, security_scan`;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Build the system prompt for L3 operational planning.
|
|
523
|
+
* Instructs the LLM to plan exact tool invocations for a task.
|
|
524
|
+
*/
|
|
525
|
+
buildOperationalPrompt(task, currentState) {
|
|
526
|
+
const stateSection = currentState
|
|
527
|
+
? `\n## Current State\n${JSON.stringify(currentState, null, 2)}`
|
|
528
|
+
: "";
|
|
529
|
+
return `You are a developer writing step-by-step tool actions. Plan exact tool calls for this task.
|
|
530
|
+
|
|
531
|
+
## Task
|
|
532
|
+
${task.description}
|
|
533
|
+
|
|
534
|
+
## Target Files
|
|
535
|
+
${task.targetFiles.join(", ")}
|
|
536
|
+
|
|
537
|
+
## Read Files
|
|
538
|
+
${task.readFiles.join(", ")}
|
|
539
|
+
|
|
540
|
+
## Available Tools
|
|
541
|
+
${task.toolStrategy.join(", ")}
|
|
542
|
+
${stateSection}
|
|
543
|
+
|
|
544
|
+
## Instructions
|
|
545
|
+
- Plan each action as a specific tool call with arguments
|
|
546
|
+
- Include expected outcomes for verification
|
|
547
|
+
- Add fallback actions for risky operations
|
|
548
|
+
- Keep actions atomic and verifiable
|
|
549
|
+
- Maximum ${this.config.maxActionsPerTask} actions
|
|
550
|
+
|
|
551
|
+
## Output Format
|
|
552
|
+
Respond with ONLY a JSON object (no markdown fences):
|
|
553
|
+
{
|
|
554
|
+
"actions": [
|
|
555
|
+
{
|
|
556
|
+
"type": "read",
|
|
557
|
+
"description": "Read current file contents",
|
|
558
|
+
"tool": "file_read",
|
|
559
|
+
"toolInput": { "path": "src/foo.ts" },
|
|
560
|
+
"expectedOutcome": "File contents loaded for analysis"
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
"type": "edit",
|
|
564
|
+
"description": "Add import statement",
|
|
565
|
+
"tool": "file_edit",
|
|
566
|
+
"toolInput": { "path": "src/foo.ts", "old_string": "...", "new_string": "..." },
|
|
567
|
+
"expectedOutcome": "Import added successfully",
|
|
568
|
+
"fallbackAction": {
|
|
569
|
+
"type": "write",
|
|
570
|
+
"description": "Rewrite file if edit fails",
|
|
571
|
+
"tool": "file_write",
|
|
572
|
+
"toolInput": { "path": "src/foo.ts", "content": "..." },
|
|
573
|
+
"expectedOutcome": "File rewritten"
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
]
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
Action types: read, analyze, write, edit, execute, test, verify`;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Build the system prompt for re-planning after failure.
|
|
583
|
+
*/
|
|
584
|
+
buildReplanPrompt(plan, trigger) {
|
|
585
|
+
const affectedTasks = plan.tactical
|
|
586
|
+
.filter((t) => trigger.affectedTaskIds.includes(t.id))
|
|
587
|
+
.map((t) => ` - ${t.id}: ${t.description}`)
|
|
588
|
+
.join("\n");
|
|
589
|
+
const completedTasks = plan.tactical
|
|
590
|
+
.filter((t) => !trigger.affectedTaskIds.includes(t.id))
|
|
591
|
+
.map((t) => ` - ${t.id}: ${t.description}`)
|
|
592
|
+
.join("\n");
|
|
593
|
+
return `You are an engineering manager deciding how to recover from a problem during task execution.
|
|
594
|
+
|
|
595
|
+
## Original Goal
|
|
596
|
+
${plan.goal}
|
|
597
|
+
|
|
598
|
+
## Problem
|
|
599
|
+
Type: ${trigger.type}
|
|
600
|
+
Severity: ${trigger.severity}
|
|
601
|
+
Description: ${trigger.description}
|
|
602
|
+
|
|
603
|
+
## Affected Tasks
|
|
604
|
+
${affectedTasks || " (none)"}
|
|
605
|
+
|
|
606
|
+
## Completed Tasks
|
|
607
|
+
${completedTasks || " (none)"}
|
|
608
|
+
|
|
609
|
+
## Instructions
|
|
610
|
+
Decide the best recovery strategy:
|
|
611
|
+
- "retry_with_fix": retry the failed task with modifications
|
|
612
|
+
- "alternative_approach": try a completely different approach
|
|
613
|
+
- "skip_and_continue": skip the failed task and continue with remaining
|
|
614
|
+
- "escalate": the problem requires user intervention
|
|
615
|
+
- "full_replan": discard remaining plan and create a new one
|
|
616
|
+
|
|
617
|
+
## Output Format
|
|
618
|
+
Respond with ONLY a JSON object (no markdown fences):
|
|
619
|
+
{
|
|
620
|
+
"strategy": "retry_with_fix",
|
|
621
|
+
"reason": "explanation of the decision",
|
|
622
|
+
"modifiedTasks": [
|
|
623
|
+
{
|
|
624
|
+
"description": "modified task description",
|
|
625
|
+
"targetFiles": ["..."],
|
|
626
|
+
"readFiles": ["..."],
|
|
627
|
+
"toolStrategy": ["..."],
|
|
628
|
+
"dependsOn": [],
|
|
629
|
+
"estimatedIterations": 3,
|
|
630
|
+
"modelPreference": "premium"
|
|
631
|
+
}
|
|
632
|
+
]
|
|
633
|
+
}`;
|
|
634
|
+
}
|
|
635
|
+
// ─── Private: Response Parsers ───
|
|
636
|
+
/**
|
|
637
|
+
* Parse LLM JSON response into a StrategicGoal.
|
|
638
|
+
* Falls back to a minimal goal on parse failure.
|
|
639
|
+
*/
|
|
640
|
+
parseStrategicResponse(content, fallbackGoal) {
|
|
641
|
+
const goalId = `goal-${randomUUID().slice(0, 8)}`;
|
|
642
|
+
const json = this.extractJson(content);
|
|
643
|
+
if (json) {
|
|
644
|
+
try {
|
|
645
|
+
const parsed = JSON.parse(json);
|
|
646
|
+
const rawSubGoals = (parsed.subGoals ?? parsed.sub_goals ?? []);
|
|
647
|
+
const subGoals = rawSubGoals.map((sg, i) => ({
|
|
648
|
+
id: `task-${randomUUID().slice(0, 8)}`,
|
|
649
|
+
goalId,
|
|
650
|
+
description: sg.description ?? "",
|
|
651
|
+
targetFiles: sg.targetFiles ?? [],
|
|
652
|
+
readFiles: sg.readFiles ?? [],
|
|
653
|
+
toolStrategy: sg.toolStrategy ?? ["file_read", "file_edit"],
|
|
654
|
+
order: i,
|
|
655
|
+
dependsOn: sg.dependsOn ?? [],
|
|
656
|
+
estimatedIterations: sg.estimatedIterations ?? 5,
|
|
657
|
+
modelPreference: sg.modelPreference,
|
|
658
|
+
}));
|
|
659
|
+
const rawRisk = (parsed.riskAssessment ?? {});
|
|
660
|
+
return {
|
|
661
|
+
id: goalId,
|
|
662
|
+
description: parsed.description ?? fallbackGoal,
|
|
663
|
+
subGoals,
|
|
664
|
+
estimatedComplexity: this.normalizeComplexity(parsed.complexity ?? "moderate"),
|
|
665
|
+
requiredCapabilities: parsed.requiredCapabilities ?? [],
|
|
666
|
+
riskAssessment: {
|
|
667
|
+
level: this.normalizeRiskLevel(rawRisk.level ?? "low"),
|
|
668
|
+
factors: rawRisk.factors ?? [],
|
|
669
|
+
mitigations: rawRisk.mitigations ?? [],
|
|
670
|
+
requiresApproval: rawRisk.requiresApproval ?? false,
|
|
671
|
+
},
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
catch {
|
|
675
|
+
// fall through to default
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
// Default: single sub-goal wrapping the entire request
|
|
679
|
+
return {
|
|
680
|
+
id: goalId,
|
|
681
|
+
description: fallbackGoal,
|
|
682
|
+
subGoals: [
|
|
683
|
+
{
|
|
684
|
+
id: `task-${randomUUID().slice(0, 8)}`,
|
|
685
|
+
goalId,
|
|
686
|
+
description: fallbackGoal,
|
|
687
|
+
targetFiles: [],
|
|
688
|
+
readFiles: [],
|
|
689
|
+
toolStrategy: ["file_read", "file_edit"],
|
|
690
|
+
order: 0,
|
|
691
|
+
dependsOn: [],
|
|
692
|
+
estimatedIterations: 5,
|
|
693
|
+
},
|
|
694
|
+
],
|
|
695
|
+
estimatedComplexity: "moderate",
|
|
696
|
+
requiredCapabilities: ["file_read", "file_edit"],
|
|
697
|
+
riskAssessment: {
|
|
698
|
+
level: "low",
|
|
699
|
+
factors: [],
|
|
700
|
+
mitigations: [],
|
|
701
|
+
requiresApproval: false,
|
|
702
|
+
},
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Parse LLM JSON response into TacticalTask[].
|
|
707
|
+
*/
|
|
708
|
+
parseTacticalResponse(content, goalId, parentSubGoal) {
|
|
709
|
+
const json = this.extractJson(content);
|
|
710
|
+
if (json) {
|
|
711
|
+
try {
|
|
712
|
+
const parsed = JSON.parse(json);
|
|
713
|
+
const rawTasks = (parsed.tasks ?? []);
|
|
714
|
+
return rawTasks.map((t, i) => ({
|
|
715
|
+
id: `task-${randomUUID().slice(0, 8)}`,
|
|
716
|
+
goalId,
|
|
717
|
+
description: t.description ?? "",
|
|
718
|
+
targetFiles: t.targetFiles ?? [],
|
|
719
|
+
readFiles: t.readFiles ?? [],
|
|
720
|
+
toolStrategy: t.toolStrategy ?? ["file_read", "file_edit"],
|
|
721
|
+
order: i,
|
|
722
|
+
dependsOn: t.dependsOn ?? [],
|
|
723
|
+
estimatedIterations: t.estimatedIterations ?? 5,
|
|
724
|
+
modelPreference: t.modelPreference,
|
|
725
|
+
}));
|
|
726
|
+
}
|
|
727
|
+
catch {
|
|
728
|
+
// fall through
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
// Default: return the parent sub-goal as a single task
|
|
732
|
+
return [
|
|
733
|
+
{
|
|
734
|
+
...parentSubGoal,
|
|
735
|
+
id: `task-${randomUUID().slice(0, 8)}`,
|
|
736
|
+
goalId,
|
|
737
|
+
},
|
|
738
|
+
];
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Parse LLM JSON response into OperationalAction[].
|
|
742
|
+
*/
|
|
743
|
+
parseOperationalResponse(content, taskId) {
|
|
744
|
+
const json = this.extractJson(content);
|
|
745
|
+
if (json) {
|
|
746
|
+
try {
|
|
747
|
+
const parsed = JSON.parse(json);
|
|
748
|
+
const rawActions = (parsed.actions ?? []);
|
|
749
|
+
return rawActions.map((a) => {
|
|
750
|
+
const action = {
|
|
751
|
+
id: `action-${randomUUID().slice(0, 8)}`,
|
|
752
|
+
taskId,
|
|
753
|
+
type: this.normalizeActionType(a.type ?? "read"),
|
|
754
|
+
description: a.description ?? "",
|
|
755
|
+
tool: a.tool ?? "file_read",
|
|
756
|
+
toolInput: a.toolInput ?? {},
|
|
757
|
+
expectedOutcome: a.expectedOutcome ?? "",
|
|
758
|
+
};
|
|
759
|
+
// Parse fallback action if present
|
|
760
|
+
if (a.fallbackAction) {
|
|
761
|
+
const fb = a.fallbackAction;
|
|
762
|
+
action.fallbackAction = {
|
|
763
|
+
id: `action-${randomUUID().slice(0, 8)}`,
|
|
764
|
+
taskId,
|
|
765
|
+
type: this.normalizeActionType(fb.type ?? "write"),
|
|
766
|
+
description: fb.description ?? "",
|
|
767
|
+
tool: fb.tool ?? "file_write",
|
|
768
|
+
toolInput: fb.toolInput ?? {},
|
|
769
|
+
expectedOutcome: fb.expectedOutcome ?? "",
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
return action;
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
catch {
|
|
776
|
+
// fall through
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
// Default: single read action
|
|
780
|
+
return [
|
|
781
|
+
{
|
|
782
|
+
id: `action-${randomUUID().slice(0, 8)}`,
|
|
783
|
+
taskId,
|
|
784
|
+
type: "read",
|
|
785
|
+
description: "Read target files",
|
|
786
|
+
tool: "file_read",
|
|
787
|
+
toolInput: {},
|
|
788
|
+
expectedOutcome: "Files loaded for analysis",
|
|
789
|
+
},
|
|
790
|
+
];
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Parse re-plan LLM response into RePlanResult.
|
|
794
|
+
*/
|
|
795
|
+
parseReplanResponse(content, currentPlan, trigger) {
|
|
796
|
+
const json = this.extractJson(content);
|
|
797
|
+
if (json) {
|
|
798
|
+
try {
|
|
799
|
+
const parsed = JSON.parse(json);
|
|
800
|
+
const rawTasks = (parsed.modifiedTasks ?? []);
|
|
801
|
+
const modifiedTasks = rawTasks.map((t, i) => ({
|
|
802
|
+
id: `task-${randomUUID().slice(0, 8)}`,
|
|
803
|
+
goalId: currentPlan.strategic.id,
|
|
804
|
+
description: t.description ?? "",
|
|
805
|
+
targetFiles: t.targetFiles ?? [],
|
|
806
|
+
readFiles: t.readFiles ?? [],
|
|
807
|
+
toolStrategy: t.toolStrategy ?? ["file_read", "file_edit"],
|
|
808
|
+
order: i,
|
|
809
|
+
dependsOn: t.dependsOn ?? [],
|
|
810
|
+
estimatedIterations: t.estimatedIterations ?? 5,
|
|
811
|
+
modelPreference: t.modelPreference,
|
|
812
|
+
}));
|
|
813
|
+
return {
|
|
814
|
+
strategy: this.normalizeReplanStrategy(parsed.strategy ?? "retry_with_fix"),
|
|
815
|
+
modifiedTasks,
|
|
816
|
+
reason: parsed.reason ?? "Re-plan generated by LLM",
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
catch {
|
|
820
|
+
// fall through
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
// Default: escalate on parse failure
|
|
824
|
+
return {
|
|
825
|
+
strategy: trigger.severity === "critical" ? "escalate" : "retry_with_fix",
|
|
826
|
+
modifiedTasks: currentPlan.tactical.filter((t) => trigger.affectedTaskIds.includes(t.id)),
|
|
827
|
+
reason: "Could not parse re-plan response; defaulting to safe strategy",
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
// ─── Private: Risk Assessment ───
|
|
831
|
+
/**
|
|
832
|
+
* Assess risk based on the goal description and target files.
|
|
833
|
+
*/
|
|
834
|
+
assessRisk(goal, files) {
|
|
835
|
+
const factors = [];
|
|
836
|
+
const mitigations = [];
|
|
837
|
+
let riskScore = 0; // 0=low, 1=medium, 2=high, 3=critical
|
|
838
|
+
// Check files against high-risk patterns
|
|
839
|
+
for (const file of files) {
|
|
840
|
+
for (const pattern of HIGH_RISK_PATTERNS) {
|
|
841
|
+
if (pattern.test(file)) {
|
|
842
|
+
factors.push(`Modifies sensitive file: ${file}`);
|
|
843
|
+
riskScore = Math.max(riskScore, 1);
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
// Check goal for destructive keywords
|
|
849
|
+
const destructiveKeywords = /\b(delete|remove|drop|reset|force|overwrite|wipe|migrate)\b/i;
|
|
850
|
+
if (destructiveKeywords.test(goal)) {
|
|
851
|
+
factors.push("Goal contains destructive operation keywords");
|
|
852
|
+
riskScore = Math.min(riskScore + 1, 3);
|
|
853
|
+
mitigations.push("Create backup before executing");
|
|
854
|
+
}
|
|
855
|
+
// Check for destructive tool usage
|
|
856
|
+
const goalLower = goal.toLowerCase();
|
|
857
|
+
if (DESTRUCTIVE_OPS.some((op) => goalLower.includes(op))) {
|
|
858
|
+
factors.push("Requires write/execute operations");
|
|
859
|
+
riskScore = Math.max(riskScore, 1);
|
|
860
|
+
mitigations.push("Verify changes with tests after modification");
|
|
861
|
+
}
|
|
862
|
+
const riskLevels = ["low", "medium", "high", "critical"];
|
|
863
|
+
const maxRisk = riskLevels[Math.min(riskScore, 3)];
|
|
864
|
+
// Determine approval requirement
|
|
865
|
+
const requiresApproval = maxRisk === "high" || maxRisk === "critical";
|
|
866
|
+
if (requiresApproval) {
|
|
867
|
+
mitigations.push("Request user approval before executing");
|
|
868
|
+
}
|
|
869
|
+
return { level: maxRisk, factors, mitigations, requiresApproval };
|
|
870
|
+
}
|
|
871
|
+
// ─── Private: Dependency Analysis ───
|
|
872
|
+
/**
|
|
873
|
+
* Sort tasks by dependency order and assign sequential order values.
|
|
874
|
+
* Uses Kahn's algorithm for topological sorting.
|
|
875
|
+
*/
|
|
876
|
+
buildDependencyChain(tasks) {
|
|
877
|
+
if (tasks.length === 0)
|
|
878
|
+
return [];
|
|
879
|
+
const taskMap = new Map(tasks.map((t) => [t.id, t]));
|
|
880
|
+
// Compute in-degree for each task (only count internal deps)
|
|
881
|
+
const inDegree = new Map();
|
|
882
|
+
for (const task of tasks) {
|
|
883
|
+
if (!inDegree.has(task.id))
|
|
884
|
+
inDegree.set(task.id, 0);
|
|
885
|
+
for (const dep of task.dependsOn) {
|
|
886
|
+
if (taskMap.has(dep)) {
|
|
887
|
+
inDegree.set(task.id, (inDegree.get(task.id) ?? 0) + 1);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
// Kahn's algorithm
|
|
892
|
+
const queue = [];
|
|
893
|
+
for (const [id, degree] of inDegree) {
|
|
894
|
+
if (degree === 0)
|
|
895
|
+
queue.push(id);
|
|
896
|
+
}
|
|
897
|
+
const sorted = [];
|
|
898
|
+
let order = 0;
|
|
899
|
+
while (queue.length > 0) {
|
|
900
|
+
const current = queue.shift();
|
|
901
|
+
const task = taskMap.get(current);
|
|
902
|
+
if (!task)
|
|
903
|
+
continue;
|
|
904
|
+
task.order = order++;
|
|
905
|
+
sorted.push(task);
|
|
906
|
+
// Reduce in-degree for dependents
|
|
907
|
+
for (const other of tasks) {
|
|
908
|
+
if (other.dependsOn.includes(current) && taskMap.has(other.id)) {
|
|
909
|
+
const newDegree = (inDegree.get(other.id) ?? 1) - 1;
|
|
910
|
+
inDegree.set(other.id, newDegree);
|
|
911
|
+
if (newDegree === 0) {
|
|
912
|
+
queue.push(other.id);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
// Append any remaining tasks (cycle members) at the end
|
|
918
|
+
for (const task of tasks) {
|
|
919
|
+
if (!sorted.includes(task)) {
|
|
920
|
+
task.order = order++;
|
|
921
|
+
sorted.push(task);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return sorted;
|
|
925
|
+
}
|
|
926
|
+
// ─── Private: Helpers ───
|
|
927
|
+
/**
|
|
928
|
+
* Extract the first JSON object or array from a response string.
|
|
929
|
+
* Handles common LLM output patterns (markdown fences, preamble text).
|
|
930
|
+
*/
|
|
931
|
+
extractJson(content) {
|
|
932
|
+
// Strip markdown code fences if present
|
|
933
|
+
const fenced = content.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
934
|
+
if (fenced) {
|
|
935
|
+
return fenced[1].trim();
|
|
936
|
+
}
|
|
937
|
+
// Find first { ... } block
|
|
938
|
+
const objectMatch = content.match(/\{[\s\S]*\}/);
|
|
939
|
+
if (objectMatch) {
|
|
940
|
+
return objectMatch[0];
|
|
941
|
+
}
|
|
942
|
+
return null;
|
|
943
|
+
}
|
|
944
|
+
/** Normalize complexity string to valid union type */
|
|
945
|
+
normalizeComplexity(value) {
|
|
946
|
+
const valid = new Set(["trivial", "simple", "moderate", "complex", "massive"]);
|
|
947
|
+
return valid.has(value)
|
|
948
|
+
? value
|
|
949
|
+
: "moderate";
|
|
950
|
+
}
|
|
951
|
+
/** Normalize risk level string to valid union type */
|
|
952
|
+
normalizeRiskLevel(value) {
|
|
953
|
+
const valid = new Set(["low", "medium", "high", "critical"]);
|
|
954
|
+
return valid.has(value) ? value : "low";
|
|
955
|
+
}
|
|
956
|
+
/** Normalize action type string to valid union type */
|
|
957
|
+
normalizeActionType(value) {
|
|
958
|
+
const valid = new Set([
|
|
959
|
+
"read",
|
|
960
|
+
"analyze",
|
|
961
|
+
"write",
|
|
962
|
+
"edit",
|
|
963
|
+
"execute",
|
|
964
|
+
"test",
|
|
965
|
+
"verify",
|
|
966
|
+
]);
|
|
967
|
+
return valid.has(value) ? value : "read";
|
|
968
|
+
}
|
|
969
|
+
/** Normalize re-plan strategy string to valid union type */
|
|
970
|
+
normalizeReplanStrategy(value) {
|
|
971
|
+
const valid = new Set([
|
|
972
|
+
"retry_with_fix",
|
|
973
|
+
"alternative_approach",
|
|
974
|
+
"skip_and_continue",
|
|
975
|
+
"escalate",
|
|
976
|
+
"full_replan",
|
|
977
|
+
]);
|
|
978
|
+
return valid.has(value) ? value : "retry_with_fix";
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
//# sourceMappingURL=hierarchical-planner.js.map
|