Planning
How Sophon decomposes complex requests into a DAG of steps and executes them in parallel with retries, cascades, and cancellation.
When you ask Sophon something complex — "research three competitors, build a comparison spreadsheet, and email it to my team" — the agent doesn't just start typing. It plans: it breaks the work into steps, figures out which steps depend on which, and executes independent steps in parallel. If a step fails, it either retries with an alternative approach or cascades cleanly so other steps still run.
Planning is middleware #7 in the orchestration pipeline.
When planning kicks in
Four modes, set via AgentExecutionOptions or per-request:
| Mode | Behavior |
|---|---|
Auto (default) | Heuristic classifier scores the message; score ≥ 3 triggers planning |
LlmDecides | Fast-tier LLM is asked "is this complex?" (3-token answer, temp 0). Falls back to heuristic on error |
Always | Every request gets planned |
Never | Planning disabled; always flat agentic loop |
The heuristic
HeuristicComplexityClassifier adds signal scores:
| Signal | Points |
|---|---|
| Message length > 100 chars | +1 |
| Contains conjunctions ("and then", "after that", "first", "then") | +2 (once) |
| Contains multi-step phrases ("steps", "plan", "sequence") | +2 (once) |
| Contains ≥ 2 action verbs (search, create, send, write, …) | +1 |
Score ≥ 3 means "complex, plan it."
Planning is skipped if the pipeline is already 3 levels deep (MaxDepth = 3). This prevents infinite plan-nesting when a plan step itself triggers planning.
What a plan looks like
An ExecutionPlan is a goal + a DAG of steps:
public sealed record ExecutionPlan(
string Id, // "plan_<guid>"
string Goal, // User's request summary
IReadOnlyList<PlanStep> Steps);
public sealed record PlanStep(
string Id,
string Description,
IReadOnlyList<string> DependsOn, // Step IDs this step waits on
IReadOnlyList<string> ToolHints, // Suggested tools
RiskLevel EstimatedRisk = RiskLevel.Low,
CapabilityRequirement? ModelHint = null);A trimmed example:
{
"id": "plan_abc123",
"goal": "Compare three competitors and email the team",
"steps": [
{ "id": "s1", "description": "Research Acme Corp",
"dependsOn": [], "toolHints": ["web.search", "browser.navigate"] },
{ "id": "s2", "description": "Research Globex",
"dependsOn": [], "toolHints": ["web.search", "browser.navigate"] },
{ "id": "s3", "description": "Research Initech",
"dependsOn": [], "toolHints": ["web.search", "browser.navigate"] },
{ "id": "s4", "description": "Build comparison spreadsheet",
"dependsOn": ["s1", "s2", "s3"], "toolHints": ["document.create"] },
{ "id": "s5", "description": "Email the team",
"dependsOn": ["s4"], "toolHints": ["gmail.send"],
"estimatedRisk": "Medium" }
]
}s1, s2, s3 have no dependencies → they run in parallel. s4 waits for all three research steps. s5 waits for the spreadsheet. And because s5 is Medium risk, the whole plan needs user approval before anything runs.
Execution
PlanExecutor uses Kahn's algorithm to topologically order the DAG. In each wave, every step whose dependencies are satisfied runs concurrently via Task.WhenAll.
Retries
On step failure, the executor asks a fast-tier LLM for an alternative approach (max 1 retry per step, 256 tokens, temp 0.5). If the LLM responds NO_ALTERNATIVE, the step is marked failed.
Smart cascade
A failed step doesn't poison the whole plan. Steps that depended on it are marked Skipped, but unrelated steps continue. If the spreadsheet step above failed, the email step would be skipped but nothing upstream would be rolled back.
Cancellation
TryCancelPlan(planId) cancels an active plan via a ConcurrentDictionary<string, CancellationTokenSource>. In-flight steps observe the token and stop at their next cancellation point.
Status events
The executor emits events the UI can subscribe to:
plan_start,plan_complete,plan_failedplan_step_start,plan_step_complete,plan_step_failed,plan_step_skipped,plan_step_retry
Clients (Dashboard, Mobile) subscribe via SignalR and render live progress — you see each step tick from pending → running → done.
Approval
If any step has EstimatedRisk >= Medium, the entire plan is gated behind a single approval request. The user sees the full plan with all steps, can approve, reject, or edit (timeout: 10 minutes). Editing opens the plan in the Workflow Builder pre-populated.
Risk is computed per step, but approval is plan-level — you don't get interrupted in the middle of execution. See Approval Gates.
Model hints
A plan step can declare capability requirements:
{
"id": "s4",
"description": "Analyze the results and write a two-paragraph executive summary",
"modelHint": { "requiresReasoning": true }
}The Capability Routing middleware picks a provider/model that advertises SupportsReasoning for that step. If no provider qualifies, the step errors rather than silently falling back to an unqualified model.
Dynamic tool chains
Related to planning is the tool.create_chain meta-tool, which lets the LLM compose existing tools into a new tool at runtime:
{
"name": "research_and_save",
"description": "Search, scrape, and save to memory",
"steps": [
{ "toolName": "web.search", "arguments": { "query": "{{input.topic}}" } },
{ "toolName": "browser.navigate", "arguments": { "url": "{{previous_result.top_url}}" } },
{ "toolName": "memory.write", "arguments": { "content": "{{previous_result.text}}" } }
]
}Chain risk is the max of all constituent tool risks, and every referenced tool is validated before the chain is registered.
Configuration
{
"Sophon": {
"AgentExecution": {
"PlanMaxSteps": 20
}
}
}The planning mode is per-agent; set it in the agent's config or via sophon agents edit <id> --planning-mode Auto|LlmDecides|Always|Never.
Where to go next
- Orchestration Pipeline — how planning fits into the broader request flow
- Approval Gates — why Medium+ risk gates the plan