Tasks
Unified task system for planning, tracking, and parallel execution
Tasks
A3S Code uses a unified Task struct for both planning and todo tracking. The Task type replaces the former separate PlanStep and Todo types — whether the agent is decomposing a complex request into an execution plan or maintaining a session-scoped task list, the same model is used throughout.
Task Model
Each task has the following fields:
Prop
Type
TaskStatus
Prop
Type
TaskPriority
Prop
Type
Task Lifecycle
Tasks transition through statuses as the agent works:
Pending → InProgress → Completed
→ Failed
→ Skipped
→ CancelledThe agent emits a TaskUpdated event whenever the task list changes, so streaming consumers can track progress in real time.
Execution Plans
For complex tasks, the planning system creates an ExecutionPlan — an ordered set of tasks with dependency awareness.
Plan Structure
ExecutionPlan
├── goal: "Refactor auth module to use JWT"
├── complexity: Complex
├── steps:
│ ├── 1. Analyze current auth implementation [Completed]
│ ├── 2. Design JWT token structure [InProgress]
│ ├── 3. Implement JWT generation and validation [Pending] (depends on 2)
│ ├── 4. Update middleware to use JWT [Pending] (depends on 3)
│ ├── 5. Update tests [Pending] (depends on 4)
│ └── 6. Verify all tests pass [Pending] (depends on 5)
└── estimated_steps: 6Complexity Levels
The planner evaluates task complexity to choose an execution strategy:
Prop
Type
The AUTO strategy uses task analysis to dynamically select the best approach.
Dependency-Aware Execution
ExecutionPlan::get_ready_steps() returns only tasks whose dependencies are all Completed. This ensures correct execution order without manual sequencing:
let ready = plan.get_ready_steps();
// Returns tasks where all dependencies are satisfiedProgress is calculated automatically:
let progress = plan.progress(); // 0.0 to 1.0Parallel Wave-Based Execution
Plan steps execute in waves based on the dependency graph. Steps with no unmet dependencies are grouped into a wave and executed concurrently via tokio::task::JoinSet. This is automatic — no extra configuration beyond enabling planning is required.
ExecutionPlan:
step 1: Analyze auth module (no deps)
step 2: Analyze database schema (no deps)
step 3: Implement JWT integration (depends on 1, 2)
step 4: Write tests (depends on 3)
Execution:
Wave 1: [step 1, step 2] ← parallel (independent)
Wave 2: [step 3] ← waits for wave 1
Wave 3: [step 4] ← waits for wave 2How It Works
get_ready_steps() identifies steps whose dependencies are all CompletedJoinSet, each with a clone of the base historyFailed, dependent steps become unreachable (deadlock detection breaks the loop)History Merging
Sequential: step1.result → context for step2 → context for step3
Parallel: step1.result + step2.result → merged context for step3Parallel steps each start from the same base history. They cannot see each other's intermediate results, which is correct — the planner explicitly declared them independent. After the wave completes, a deterministically sorted summary of all parallel results is injected into the shared history so subsequent steps can reference everything.
Streaming Parallel Execution Events
The executor emits StepStart, StepEnd, and GoalProgress events during wave-based execution. Streaming consumers can track which steps are running in parallel and monitor overall progress.
use a3s_code_core::{Agent, AgentEvent, SessionOptions};
let agent = Agent::new("agent.hcl").await?;
let session = agent.session("/my-project", Some(
SessionOptions::new()
.with_planning(true)
.with_goal_tracking(true)
))?;
let (mut rx, _handle) = session.stream("Refactor auth to use JWT and update all tests").await?;
while let Some(event) = rx.recv().await {
match event {
AgentEvent::StepStart { step_id, description, step_number, total_steps } => {
// During parallel waves, multiple StepStart events fire before any StepEnd
println!("[{step_number}/{total_steps}] Starting: {description}");
}
AgentEvent::StepEnd { step_id, status, step_number, total_steps } => {
println!("[{step_number}/{total_steps}] {status}");
}
AgentEvent::GoalProgress { goal, progress, completed_steps, total_steps } => {
// Fires after each wave completes
println!("Goal: {goal}");
println!("Progress: {:.0}% ({completed_steps}/{total_steps})", progress * 100.0);
}
AgentEvent::TextDelta { text } => print!("{text}"),
AgentEvent::End { .. } => break,
_ => {} // required: AgentEvent is #[non_exhaustive]
}
}const { Agent } = require('@a3s-lab/code');
const agent = await Agent.create('agent.hcl');
const session = agent.session('/my-project', {
planning: true,
goalTracking: true,
});
const events = await session.stream('Refactor auth to use JWT and update all tests');
for (const event of events) {
switch (event.type) {
case 'step_start':
// During parallel waves, multiple step_start events fire before any step_end
console.log(`[${event.stepNumber}/${event.totalSteps}] Starting: ${event.description}`);
break;
case 'step_end':
console.log(`[${event.stepNumber}/${event.totalSteps}] ${event.status}`);
break;
case 'goal_progress':
// Fires after each wave completes
console.log(`Progress: ${(event.progress * 100).toFixed(0)}% (${event.completedSteps}/${event.totalSteps})`);
break;
case 'text_delta':
process.stdout.write(event.text);
break;
}
}from a3s_code import Agent
agent = Agent.create("agent.hcl")
session = agent.session("/my-project", planning=True, goal_tracking=True)
for event in session.stream("Refactor auth to use JWT and update all tests"):
if event.event_type == "step_start":
# During parallel waves, multiple step_start events fire before any step_end
print(f"[{event.step_number}/{event.total_steps}] Starting: {event.description}")
elif event.event_type == "step_end":
print(f"[{event.step_number}/{event.total_steps}] {event.status}")
elif event.event_type == "goal_progress":
# Fires after each wave completes
print(f"Progress: {event.progress:.0%} ({event.completed_steps}/{event.total_steps})")
elif event.event_type == "text_delta":
print(event.text, end="", flush=True)Example output for the plan above:
[1/4] Starting: Analyze auth module ← wave 1 (parallel)
[2/4] Starting: Analyze database schema ← wave 1 (parallel)
[2/4] completed ← step 2 finishes first
[1/4] completed ← step 1 finishes
Progress: 50% (2/4) ← wave 1 done
[3/4] Starting: Implement JWT integration ← wave 2
[3/4] completed
Progress: 75% (3/4)
[4/4] Starting: Write tests ← wave 3
[4/4] completed
Progress: 100% (4/4)Note how steps 1 and 2 emit StepStart events simultaneously — they are running in parallel. The order of StepEnd events for parallel steps is non-deterministic (whichever finishes first).
Dependency Graph API
use a3s_code_core::planning::{ExecutionPlan, Task, TaskStatus, Complexity};
let mut plan = ExecutionPlan::new("Refactor auth", Complexity::Complex);
// Independent steps — will run in parallel (Wave 1)
plan.add_step(Task::new("s1", "Analyze auth module"));
plan.add_step(Task::new("s2", "Analyze database schema"));
// Dependent step — waits for Wave 1 (Wave 2)
plan.add_step(
Task::new("s3", "Implement JWT")
.with_dependencies(vec!["s1".to_string(), "s2".to_string()])
);
// Wave 1: both s1 and s2 are ready
assert_eq!(plan.get_ready_steps().len(), 2);
// Simulate completing wave 1
plan.mark_status("s1", TaskStatus::Completed);
plan.mark_status("s2", TaskStatus::Completed);
// Wave 2: s3 is now ready
assert_eq!(plan.get_ready_steps().len(), 1);
assert_eq!(plan.get_ready_steps()[0].id, "s3");
// Progress tracking
assert!((plan.progress() - 0.666).abs() < 0.01); // 2/3
// Deadlock detection
assert!(!plan.has_deadlock());
// Count remaining work
assert_eq!(plan.pending_count(), 1);// The dependency graph is built by the LLM planner automatically.
// In TypeScript, you configure planning and observe events:
const session = agent.session('/my-project', {
planning: true, // LLM decomposes task into steps with dependencies
goalTracking: true, // track progress against success criteria
});
// The planner produces a JSON plan like:
// {
// "goal": "Refactor auth",
// "complexity": "Complex",
// "steps": [
// { "id": "s1", "content": "Analyze auth module", "dependencies": [] },
// { "id": "s2", "content": "Analyze database schema", "dependencies": [] },
// { "id": "s3", "content": "Implement JWT", "dependencies": ["s1", "s2"] }
// ]
// }
//
// The executor automatically parallelizes s1 + s2, then runs s3.# The dependency graph is built by the LLM planner automatically.
# In Python, you configure planning and observe events:
session = agent.session("/my-project", planning=True, goal_tracking=True)
# The planner produces a JSON plan like:
# {
# "goal": "Refactor auth",
# "complexity": "Complex",
# "steps": [
# {"id": "s1", "content": "Analyze auth module", "dependencies": []},
# {"id": "s2", "content": "Analyze database schema", "dependencies": []},
# {"id": "s3", "content": "Implement JWT", "dependencies": ["s1", "s2"]}
# ]
# }
#
# The executor automatically parallelizes s1 + s2, then runs s3.Helper Methods on ExecutionPlan
Prop
Type
Deadlock Detection
If pending steps remain but no steps are ready (all dependencies failed or circular), the executor detects the deadlock and breaks out of the loop, returning partial results.
// Circular dependency — immediate deadlock
plan.add_step(Task::new("a", "Step A").with_dependencies(vec!["b".to_string()]));
plan.add_step(Task::new("b", "Step B").with_dependencies(vec!["a".to_string()]));
assert!(plan.has_deadlock()); // true — neither step can start
// Failed dependency — cascading deadlock
plan.mark_status("s1", TaskStatus::Failed);
// Steps depending on s1 can never start → has_deadlock() returns trueFallback Planner Compatibility
The fallback planner creates linear chains (analyze → implement → verify), so it always produces one step per wave — effectively sequential execution. Parallel execution only activates when the LLM planner creates steps with non-overlapping dependencies.
Planning Mode
Enable planning with the planning_enabled flag in AgentConfig:
let config = AgentConfig {
planning_enabled: true,
// ...
};When enabled, the LlmPlanner generates a structured plan before execution begins. If the LLM planner fails (e.g., malformed response), a heuristic fallback creates a reasonable default plan.
LLM Planner
The LlmPlanner sends a structured prompt to the LLM asking it to decompose the task into steps. The response is parsed as JSON:
{
"goal": "Add input validation to all API endpoints",
"complexity": "Complex",
"steps": [
{ "content": "Identify all API endpoints", "priority": "high", "tool": "Grep" },
{ "content": "Add validation middleware", "priority": "high" },
{ "content": "Write tests for validation", "priority": "medium" }
]
}Fallback Planner
When the LLM planner fails, the fallback heuristic creates a 3-step plan:
Goal Tracking
When goal_tracking is enabled, the agent extracts a measurable goal from the user's prompt and monitors progress throughout execution.
Goal Structure
pub struct AgentGoal {
pub description: String,
pub success_criteria: Vec<String>,
pub progress: f32, // 0.0 to 1.0
pub achieved: bool,
pub created_at: DateTime<Utc>,
pub achieved_at: Option<DateTime<Utc>>,
}How It Works
User prompt: "Add input validation to all API endpoints"
│
├── Goal: "All API endpoints validate their input parameters"
├── Success criteria:
│ ├── Each endpoint has validation middleware
│ ├── Invalid input returns 400 with error details
│ └── Tests cover validation edge cases
└── Progress: 0% → 33% → 66% → 100%Events
Task-related events emitted during execution:
Prop
Type
External Task Offloading
Plan steps execute tool calls through the Lane Queue. Any lane can be switched to External mode, where tool calls become ExternalTask objects that remote workers poll and execute. This means multi-machine parallel execution works transparently with the planning system:
Planner decomposes task → Wave scheduler groups steps
│
├─ Wave 1: [step 1, step 2] parallel
│ ├─ step 1 tool calls → Lane Queue → External → Worker A
│ └─ step 2 tool calls → Lane Queue → External → Worker B
│
├─ Wave 2: [step 3] (depends on 1, 2)
│ └─ step 3 tool calls → Lane Queue → External → Worker C
│
└─ Results flow back via complete_external_task()No changes to the planning config are needed — enable planning as usual, and configure lane handlers to route execution to external workers. For the full external task handling API, handler modes, worker process examples, and dynamic lane switching, see Lane Queue → External Task Handling and Multi-Machine Distribution.
Storage
Tasks are stored in SessionData alongside conversation history and memory. The tasks field uses #[serde(alias = "todos")] for backward compatibility with sessions persisted before the type unification.
When a session is restored from storage, its task list is restored too — the agent picks up where it left off.
API Reference
SessionOptions
Prop
Type
AgentTask fields
Prop
Type
AgentGoal fields
Prop
Type
Planner trait (Rust)
Prop
Type