Comprehensive documentation set for XWiki: - Home, Installation, Quick Start guides - Writing Handlers and LLM Router guides - Architecture docs (Overview, Message Pump, Thread Registry, Shared Backend) - Reference docs (Configuration, Handler Contract, CLI) - Hello World tutorial - Why XML rationale - Pandoc conversion scripts (bash + PowerShell) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
5.2 KiB
5.2 KiB
Thread Registry
The Thread Registry maps opaque UUIDs to call chains, enabling thread tracking while hiding topology from handlers.
Purpose
When agents communicate, they form call chains:
console → greeter → calculator → back to greeter → shouter
The registry:
- Tracks call chains for routing responses
- Provides opaque UUIDs to handlers (hiding topology)
- Manages chain pruning when handlers respond
Concepts
Call Chain
A dot-separated path showing message flow:
system.organism.console.greeter.calculator
│ │ │ │ │
│ │ │ │ └─ Current position
│ │ │ └─ Greeter called calculator
│ │ └─ Console called greeter
│ └─ Organism name
└─ Root
Opaque UUID
What handlers actually see:
550e8400-e29b-41d4-a716-446655440000
Handlers never see the actual chain. This prevents:
- Topology probing
- Call chain forgery
- Thread hijacking
API
Initialize Root
At boot time:
from xml_pipeline.message_bus.thread_registry import get_registry
registry = get_registry()
root_uuid = registry.initialize_root("my-organism")
# Creates: system.my-organism → uuid
Get or Create
Get UUID for a chain (creates if needed):
uuid = registry.get_or_create("console.greeter")
# Returns: existing UUID or creates new one
Lookup
Get chain for a UUID:
chain = registry.lookup(uuid)
# Returns: "console.greeter" or None
Extend Chain
When forwarding to a new handler:
new_uuid = registry.extend_chain(current_uuid, "calculator")
# Before: console.greeter (uuid-123)
# After: console.greeter.calculator (uuid-456)
Prune for Response
When a handler returns .respond():
target, new_uuid = registry.prune_for_response(current_uuid)
# Before: console.greeter.calculator (uuid-456)
# After: console.greeter (uuid-123)
# target: "greeter"
Register External Thread
For messages arriving with pre-assigned UUIDs:
registry.register_thread(
thread_id="external-uuid",
initiator="console",
target="greeter"
)
# Creates: system.organism.console.greeter → external-uuid
Thread Lifecycle
Creation
1. External message arrives without thread
│
▼
2. thread_assignment_step generates UUID
│
▼
3. Registry maps: chain → UUID
Extension
1. Handler A forwards to Handler B
│
▼
2. Pump calls extend_chain(uuid_A, "B")
│
▼
3. Registry creates: chain.B → uuid_B
Pruning
1. Handler B calls .respond()
│
▼
2. Pump calls prune_for_response(uuid_B)
│
▼
3. Registry:
- Looks up chain: "...A.B"
- Prunes last segment: "...A"
- Returns target "A" and uuid_A
│
▼
4. Response routed to Handler A
Cleanup
1. Chain exhausted (root reached) or
Handler returns None
│
▼
2. UUID mapping removed
│
▼
3. Context buffer for thread deleted
Shared Backend Support
For multiprocess deployments, the registry can use a shared backend:
from xml_pipeline.memory.shared_backend import get_shared_backend, BackendConfig
# Use Redis for distributed deployments
config = BackendConfig(backend_type="redis", redis_url="redis://localhost:6379")
backend = get_shared_backend(config)
registry = get_registry(backend=backend)
Storage Schema (Redis)
xp:chain:{chain} → {uuid} # Chain to UUID
xp:uuid:{uuid} → {chain} # UUID to Chain
Security Properties
What Handlers See
metadata.thread_id = "550e8400-..." # Opaque UUID
metadata.from_id = "greeter" # Only immediate caller
What Handlers Don't See
- Full call chain
- Other thread UUIDs
- Thread count or topology
- Parent/child relationships
Why This Matters
Even compromised handlers cannot:
- Forge thread IDs — UUIDs are cryptographically random
- Discover topology — Chain hidden behind UUID
- Hijack threads — Registry validates all operations
- Probe other threads — No enumeration API
Debugging
For operators (not exposed to handlers):
# Dump all mappings
chains = registry.debug_dump()
# {'uuid-123': 'console.greeter', 'uuid-456': 'console.greeter.calc'}
# Clear (testing only)
registry.clear()
Example Flow
1. Console sends @greeter hello
├── UUID assigned: uuid-1
└── Chain: system.org.console.greeter
2. Greeter forwards to calculator
├── extend_chain(uuid-1, "calculator")
├── New UUID: uuid-2
└── Chain: system.org.console.greeter.calculator
3. Calculator responds
├── prune_for_response(uuid-2)
├── Target: "greeter"
└── UUID: uuid-1 (back to greeter's context)
4. Greeter responds
├── prune_for_response(uuid-1)
├── Target: "console"
└── Chain exhausted → cleanup
Configuration
No explicit configuration needed. The registry:
- Initializes automatically at pump startup
- Uses shared backend if configured
- Cleans up on thread termination
See Also
- Architecture Overview — High-level architecture
- Message Pump — How the pump uses the registry
- Shared Backend — Cross-process storage