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.
The architecture trace campaign documented eight AI subsystems (per
[`ai_pipeline.md`](https://github.com/lazyxeon/AstraWeave-AI-Native-Gaming-Engine/blob/main/docs/architecture/ai_pipeline.md) §13). Wired in the runtime today:

* **`astraweave-ai`** — `AIArbiter`, `Orchestrator` trait, `LlmExecutor`, `tool_sandbox`.
  Validated at 12,700+ agents @ 60 FPS.
* **`astraweave-behavior`** — canonical GOAP (`BTreeMap<u32, bool>` `WorldState`,
  interned keys, LRU plan cache) and Behavior Trees.
* **`astraweave-llm`** — Ollama client adapters. Runtime model default is
  **`phi3:medium`** (`orchestrator.rs:488-490`), set via the `OLLAMA_MODEL` env var.
  Three clients (Phi3, Hermes2Pro, Qwen3) coexist; Qwen3 is supported but is not
  the runtime default despite doc-comment phrasing like "GOAP+Qwen3 Hybrid."

Dormant in-design (passes tests, zero production callers — per
[`ai_pipeline.md`](https://github.com/lazyxeon/AstraWeave-AI-Native-Gaming-Engine/blob/main/docs/architecture/ai_pipeline.md) §13 and `ARCHITECTURE_MAP.md` §5.1):

* **Memory pipeline (~11K LoC)** — zero in-engine production consumers; only the
  legacy `persona::*` types are used.
* **Coordination crate (~5.3K)** — zero workspace consumers; three commented-out
  `pub mod` declarations point at source files that were never created.
* **Advanced GOAP (~16.7K)** — feature `planner_advanced`, parallel to the
  canonical GOAP in `astraweave-behavior` (Q2 in §14).
* **LLM Production Hardening (~15K)** — rate limiting, circuit breakers, A/B
  routing, retry, telemetry, ToolGuard, 4-tier fallback. The runtime `AIArbiter`
  path bypasses this entire surface.
* **RAG composite (~12.3K)** — `RagPipeline` held as field by five LLM-enhanced
  consumer crates, all themselves dormant. The advertised HNSW vector index in
  `astraweave-embeddings` is actually a linear scan over a DashMap.
* **Dialogue LLM layer (~2.9K)** — `llm_dialogue.rs`; the basic
  `DialogueGraph`/`DialogueRunner` path is production-wired, the LLM-enhanced layer
  is not.
* **NPC isolated subsystem (~1.7K)** — fully isolated AI subsystem with its own
  parallel vocabulary; zero imports of canonical AI types.

The interactive workspace map's *AI Pipeline Subsystem Layout* story preset
highlights the active and dormant surfaces side by side.

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