Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

AI System

AstraWeave’s AI system is the core differentiator of the engine. Unlike traditional game engines where AI is an afterthought, AstraWeave treats AI agents as first-class citizens that interact with the game world through validated tools.

In AstraWeave, AI agents cannot cheat. They must use the same validated game systems as players, ensuring fair and emergent gameplay.

Architecture Overview

graph TB
    subgraph "AI Pipeline"
        Perception[Perception Bus]
        Memory[Agent Memory]
        Planning[Planning Layer]
        Tools[Tool Sandbox]
        Validation[Engine Validation]
    end
    
    subgraph "Game World"
        ECS[ECS World State]
        Physics[Physics System]
        Nav[Navigation]
    end
    
    ECS --> Perception
    Perception --> Memory
    Memory --> Planning
    Planning --> Tools
    Tools --> Validation
    Validation --> ECS

Core Components

Perception Bus

The perception system provides AI agents with a filtered view of the world:

#![allow(unused)]
fn main() {
use astraweave_ai::perception::*;

#[derive(Component)]
struct AiAgent {
    perception_radius: f32,
    perception_filter: PerceptionFilter,
}

fn perception_system(
    agents: Query<(Entity, &Transform, &AiAgent)>,
    percievables: Query<(Entity, &Transform, &Percievable)>,
    mut perception_bus: ResMut<PerceptionBus>,
) {
    for (agent_entity, agent_transform, agent) in agents.iter() {
        let mut percepts = Vec::new();
        
        for (target, target_transform, percievable) in percievables.iter() {
            let distance = agent_transform.translation
                .distance(target_transform.translation);
            
            if distance <= agent.perception_radius {
                percepts.push(Percept {
                    entity: target,
                    position: target_transform.translation,
                    category: percievable.category,
                    properties: percievable.properties.clone(),
                });
            }
        }
        
        perception_bus.update(agent_entity, percepts);
    }
}
}

See Perception Bus for details.

Planning Layer

AI agents use LLM-based planning to decide actions:

#![allow(unused)]
fn main() {
use astraweave_ai::planning::*;

fn planning_system(
    mut agents: Query<(&mut AiPlanner, &PerceptionState)>,
    llm: Res<LlmClient>,
) {
    for (mut planner, perception) in agents.iter_mut() {
        if planner.needs_replan() {
            let context = build_context(perception);
            
            match llm.plan(&context, &planner.available_tools) {
                Ok(plan) => planner.set_plan(plan),
                Err(e) => planner.fallback_behavior(),
            }
        }
    }
}
}

See Planning Layer for details.

Tool Sandbox

All AI actions go through the tool sandbox for validation:

#![allow(unused)]
fn main() {
use astraweave_ai::tools::*;

fn execute_tool_system(
    mut agents: Query<&mut AiPlanner>,
    mut tool_executor: ResMut<ToolExecutor>,
    validator: Res<ToolValidator>,
) {
    for mut planner in agents.iter_mut() {
        if let Some(tool_call) = planner.next_action() {
            match validator.validate(&tool_call) {
                ValidationResult::Valid => {
                    tool_executor.execute(tool_call);
                }
                ValidationResult::Invalid(reason) => {
                    planner.action_failed(reason);
                }
            }
        }
    }
}
}

See Tool Sandbox for details.

Behavior Trees

For deterministic, reactive behaviors:

#![allow(unused)]
fn main() {
use astraweave_behavior::*;

let patrol_tree = BehaviorTree::new(
    Selector::new(vec![
        Sequence::new(vec![
            Condition::new(|ctx| ctx.enemy_visible()),
            Action::new(|ctx| ctx.engage_combat()),
        ]),
        Sequence::new(vec![
            Condition::new(|ctx| ctx.at_patrol_point()),
            Action::new(|ctx| ctx.next_patrol_point()),
        ]),
        Action::new(|ctx| ctx.move_to_patrol_point()),
    ])
);
}

See Behavior Trees for details.

AI Agent Configuration

Basic Agent Setup

#![allow(unused)]
fn main() {
fn spawn_companion(world: &mut World) -> Entity {
    world.spawn((
        Transform::default(),
        AiAgent::new()
            .with_personality("friendly and helpful")
            .with_perception_radius(15.0)
            .with_tick_budget(Duration::from_millis(8)),
        
        PerceptionState::default(),
        AiPlanner::new(vec![
            Tool::move_to(),
            Tool::attack(),
            Tool::use_item(),
            Tool::speak(),
        ]),
        
        NavAgent::default(),
        DialogueCapable::default(),
    ))
}
}

Tick Budget

AI has a strict time budget per simulation tick:

#![allow(unused)]
fn main() {
let config = AiConfig {
    tick_budget_ms: 8,
    max_concurrent_plans: 4,
    plan_cache_duration: Duration::from_secs(1),
    fallback_on_timeout: true,
};
}

If planning exceeds the budget, agents use cached plans or fallback behaviors.

LLM Integration

Ollama Setup

AstraWeave uses Ollama for local LLM inference:

ollama serve
ollama pull hermes2-pro-mistral

Configuration

#![allow(unused)]
fn main() {
let llm_config = LlmConfig {
    endpoint: "http://localhost:11434".into(),
    model: "hermes2-pro-mistral".into(),
    temperature: 0.7,
    max_tokens: 256,
    timeout: Duration::from_millis(100),
};
}

Tool Calling

AstraWeave uses structured tool calling:

#![allow(unused)]
fn main() {
let tools = vec![
    ToolDefinition {
        name: "move_to",
        description: "Move to a target location",
        parameters: json!({
            "type": "object",
            "properties": {
                "target": { "type": "array", "items": { "type": "number" } }
            }
        }),
    },
];

let response = llm.generate_with_tools(prompt, &tools).await?;
}

Performance Considerations

Batching

Group AI queries for efficiency:

#![allow(unused)]
fn main() {
let batch = agents.iter()
    .filter(|a| a.needs_replan())
    .take(4)
    .collect::<Vec<_>>();

let results = llm.batch_plan(&batch).await;
}

Caching

Cache plans to reduce LLM calls:

#![allow(unused)]
fn main() {
let planner = AiPlanner::new(tools)
    .with_plan_cache(Duration::from_secs(2))
    .with_context_hash(true);
}

Fallback Behaviors

Always define fallbacks:

#![allow(unused)]
fn main() {
impl AiAgent {
    fn fallback_behavior(&self) -> Action {
        match self.role {
            Role::Companion => Action::FollowPlayer,
            Role::Guard => Action::Patrol,
            Role::Merchant => Action::Idle,
        }
    }
}
}

Subsections