Components are data bags. They define what an entity has, not what it does. Processors do the doing.
Defining a Component¶
from archetype.core.component import Component
class Position(Component):
x: float = 0.0
y: float = 0.0
class Velocity(Component):
vx: float = 0.0
vy: float = 0.0
class Health(Component):
hp: int = 100
max_hp: int = 100
Components are Pydantic models backed by Arrow serialization. Every field needs a default value.
Column Prefixing¶
When components become DataFrame columns, each field is prefixed with the lowercase class name:
pos = Position(x=5, y=10)
pos.to_row_dict()
# {"position__x": 5, "position__y": 10}
In a processor, you access these columns by their prefixed names:
df.with_columns({
"position__x": col("position__x") + col("velocity__vx"),
})
Supported Field Types¶
| Python Type | Arrow-Compatible | Notes |
|---|---|---|
str, int, float, bool |
Yes | Direct |
list[str], list[int] |
Yes | Direct |
bytes |
Yes | Direct |
dict |
No | JSON-encode to str |
list[dict] |
No | JSON-encode to str |
| Custom objects | No | JSON-encode to str |
Pattern for complex types -- store as JSON strings:
class Agent(Component):
name: str = ""
role: str = ""
journal: str = "[]" # JSON list of thoughts
metadata: str = "{}" # JSON dict
tags: str = "[]" # JSON list of strings
Reading back:
import json
journal = json.loads(row["agent__journal"])
Archetype Signatures¶
Entities with the same set of component types are grouped into an archetype. An archetype is a single DataFrame table where rows are entities and columns are prefixed component fields plus metadata.
Archetype (Position, Velocity):
entity_id | tick | position__x | position__y | velocity__vx | velocity__vy
1 | 0 | 5.0 | 10.0 | 0.5 | 1.0
2 | 0 | 15.0 | 20.0 | 2.0 | 3.0
Archetype (Position):
entity_id | tick | position__x | position__y
3 | 0 | 25.0 | 30.0
Order doesn't matter -- [Position, Velocity] and [Velocity, Position] produce the same signature.
Adding and Removing Components¶
When you add or remove a component from an entity, its archetype signature changes and it moves to a different table. The old row is soft-deleted (is_active=False), a new row is inserted in the target archetype. All existing state is preserved.
# Entity starts with (Position,)
entity_id = await world.create_entity([Position(x=5, y=6)])
await world.step(run_config)
# Add Velocity — entity moves from (Position,) to (Position, Velocity)
await world.add_components(entity_id, [Velocity(vx=1, vy=1)])
await world.step(run_config)
# Remove Velocity — entity moves back to (Position,)
await world.remove_components(entity_id, [Velocity])
await world.step(run_config)
The full history is preserved for time-travel queries.
Composition Patterns¶
Agent with capabilities:
class Identity(Component):
name: str = ""
role: str = ""
class Memory(Component):
journal: str = "[]"
class Spatial(Component):
x: float = 0.0
y: float = 0.0
zone: str = "spawn"
# Basic agent
await world.create_entity([Identity(name="Scout")])
# Agent with memory
await world.create_entity([Identity(name="Historian"), Memory()])
# Agent with memory and position
await world.create_entity([Identity(name="Explorer"), Memory(), Spatial()])
Each combination creates a different archetype. Processors declare which components they need and only run on matching archetypes:
class ThinkProcessor(AsyncProcessor):
components = (Identity, Memory) # Only entities with both
priority = 10
class MoveProcessor(AsyncProcessor):
components = (Identity, Spatial) # Only entities with both
priority = 20
The Scout gets neither processor. The Historian gets ThinkProcessor only. The Explorer gets both.
Real-World Example¶
From the trajectory analysis pipeline:
class Trajectory(Component):
trajectory_id: str = ""
source: str = ""
turns_json: str = "[]" # JSON list of Turn dicts
total_turns: int = 0
total_tokens: int = 0
duration_seconds: float = 0.0
outcome: str = ""
tags_json: str = "[]"
metadata_json: str = "{}"
class Label(Component):
technique: str = ""
description: str = ""
value: str = ""
score: float = 0.0
rationale: str = ""
sampled: bool = True
An entity spawned with [Trajectory(...), Label(...)] lives in the (Label, Trajectory) archetype and has columns like trajectory__trajectory_id, label__score, etc.