A3S Docs
A3S Code

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
                     → Cancelled

The 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: 6

Complexity 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 satisfied

Progress is calculated automatically:

let progress = plan.progress(); // 0.0 to 1.0

Parallel 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 2

How It Works

Each iteration, get_ready_steps() identifies steps whose dependencies are all Completed
Single step ready → executes sequentially, preserving the full history chain
Multiple steps ready → spawns all into a JoinSet, each with a clone of the base history
After a parallel wave completes, results are sorted and merged back into the shared history
Failed steps → marked Failed, 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 step3

Parallel 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 true

Fallback 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:

Analyze — Understand the current state (priority: high)
Implement — Make the required changes (priority: high)
Verify — Confirm the changes work (priority: medium)

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

Goal extraction — The planner analyzes the user prompt and extracts a goal with success criteria
Progress monitoring — After each agent turn, the goal tracker evaluates completion
Achievement check — Compares current state against success criteria, reports remaining work
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

On this page