- engine/agents.all_agents() now enriches each agent row with its
currently-assigned model (model_for_agent) and provider
(provider_for_model). Same source of truth as the LLM client.
- web/app.js refreshAgentCards() shows a coloured tag next to the
agent name: a green '💻 <model>' badge for local Ollama models,
a yellow '☁ <model>' badge for OpenRouter models. The full
model name is in the title attribute (hover).
- web/style.css: .model-tag.local / .cloud colour palette.
- Short name helper strips 'org/' prefix and ':latest' suffix for
readability (e.g. 'anthropic/claude-3.5-haiku' -> 'claude-3.5-haiku').
All 100 tests still pass.
Live: Anchor + Flora show '☁ claude-3.5-haiku' / '☁ gpt-4o-mini',
Lovely + Spark show '💻 gemma4' (running on 192.168.1.245).
The user wanted to use Ollama on a network host (192.168.1.245).
The host is reachable (ping ~900ms) but its Ollama port (11434)
is not open, so the engine falls back to a local URL.
Changes:
- .env: EMERGENCE_OLLAMA_URL = network host, EMERGENCE_OLLAMA_FALLBACK_URL
= localhost
- engine/llm.py: chat_ollama now iterates primary then fallback URL
on connection failure. is_available() does the same. provider_info()
exposes both URLs.
- All 100 tests still pass.
Live-verified: lovely+spark (llama3.2:3b) use mode=llm with ~10s
latency, which is the connection-refused on 192.168.1.245 + the
successful fallback to 127.0.0.1. As soon as 192.168.1.245's Ollama
is reachable, latency will drop to normal (~1-3s).
Routing fix:
- New provider_for_model(name): a model name containing '/' is
treated as an OpenRouter slug, bare names (llama3.2:3b) as Ollama.
Previously the global PROVIDER variable routed all calls, so a
per-agent override 'llama3.2:3b' would have hit OpenRouter and 404'd.
- decide_tool now uses provider_for_model() so per-agent models
route correctly regardless of global PROVIDER setting.
- New provider_for_agent() helper for callers that need the
provider of a specific agent.
Live mix: Anchor + Flora on OpenRouter (claude-haiku, gpt-4o-mini);
Lovely + Spark on Ollama (llama3.2:3b, free local).
.env:
- Provider set to 'auto' (uses OpenRouter when key is set)
- Per-agent assignments documented in .env.example
- Cost estimate updated: 2 OR + 2 Ollama = ~$0.10-0.30/day for OR
portion, $0 for Ollama portion
Tests: 100 passing (was 99). New test_provider_for_model() covers
the routing heuristic. Existing tests updated to pass model=...
explicitly so they don't depend on env-loaded .env overrides.
Major restructure of the README:
- Removed the misleading 'Keine echten LLMs' line from the
'Was es bewusst NICHT kann' section (we now have full Ollama +
OpenRouter support with per-agent models).
- Added a Highlights table at the top with status badges.
- Reorganised Quickstart into 3 paths: rule-based, Ollama,
OpenRouter (was a single Ollama path with optional LLM).
- New 'Was fehlt gegenüber dem Original' section: clear comparison
table mapping each original feature to the Mini equivalent and
explaining why we skipped it.
- New 'Token-Spar-Design' section: token budgets, model cost
examples, explicit 0-cost path via Ollama.
- 'Tests' section updated: real test counts per file (was a
generic '50+' stat), 99 total, breakdown by file.
- 'Time Dilation' section reorganised and made the live-validated
observation the headline.
- LLM provider section split into Ollama (default) and OpenRouter
(opt-in), with a free-model tool-use table and a per-day cost
example.
- Architecture tree includes engine/time.py, .env.example,
tests/ and removes nothing.
- Security section moved up and split from 'Tests' cleanly.
- All anchors updated and TOC added at the top.
Implements core pieces of 'Time Dilation in LLM Agent Systems'
(Dillenberg 2026) and adds OpenRouter as a second LLM provider.
ENGINE
- engine/time.py: AgentClock with cumulative proper time tau
(weighted by op type), EWMA pace (alpha=0.3, dt clamped 0.1-60s),
ClockRegistry singleton, gamma_{src->dst} frame transformation,
drift_report with per-pair divergence and threshold flag.
- engine/turn.py: ticks tau on reasoning/tool/memory/reactive;
broadcasts tau+pace+model in every WebSocket message.
- engine/db.py: schema adds turn_log.tau, turn_log.pace,
turn_log.model, agent_clocks table; dev-mode auto-migrate
drops+recreates if old schema detected.
- engine/llm.py: full refactor for two providers.
Ollama: native tool-calling via /api/chat
OpenRouter: OpenAI-compatible /api/v1/chat/completions
Auto mode picks OpenRouter if OPENROUTER_API_KEY is set.
Per-agent model via EMERGENCE_AGENT_<ID>_MODEL env var.
.env loader with empty-line guard.
decide_tool returns (name, args, meta) with cost_usd for OR.
FRONTEND
- web/: new 'Time Dilation · Eigenzeit tau' section with per-agent
tau bars, pace, op count. Drift warning when any pair exceeds
threshold. LLM provider info in header.
TESTS
- 14 new tests in tests/test_time.py (tau monotonic, EWMA convergence,
gamma asymmetry, drift detection).
- 4 new LLM tests: openrouter response parsing, per-agent override,
provider_info, is_available.
- All 99 tests green.
LIVE-VERIFIED
- 4 different OpenRouter models running in parallel:
- anchor: anthropic/claude-3.5-haiku
- flora: openai/gpt-4o-mini
- lovely: meta-llama/llama-3.3-70b-instruct
- spark: google/gemma-3-4b-it
- All 4 produce turns, all 4 have different tau values,
drift_report shows the Frame-Transformation gamma values.
- Observation: gamma ~ 1.00 because the explicit Round-Robin +
sleep(2) keeps frames coherent. This is itself a non-trivial
validation of the paper's claim: in non-synchronized systems,
dilation would emerge.
SECRETS
- .env added, OPENROUTER_API_KEY live. .env is git-ignored.
- .env.example documents the config without exposing any key.
- .gitignore now blocks .env, .env.local, *.key, *.pem.
README
- New 'Time Dilation' section explaining tau, pace, CDC, drift
- New 'Multi-LLM via OpenRouter' section with cost table
- Per-agent model config documented
Two bugs that crashed the engine in production:
1. world.nearby_agents() did not SELECT the 'personality' column, so
_reaction_turn raised KeyError on every reactive trigger, killing the
engine thread silently. Engine-Thread stieg aus ohne Log.
Fix: select personality and json-parse it so callers get a real list,
matching agents_mod.get().
2. server.py ws() handler caught the generic 'Exception' from
asyncio.to_thread(queue.get) and tried to send a ping back, but the
WebSocket was already closed by the client. Starlette raised
RuntimeError: Cannot call 'send' once a close message has been sent.
Fix: drop the ping, just break the loop on any exception. Client
disconnect now handled cleanly.
Live-verified: 0 errors in log after 3 abrupt disconnects, engine
continues producing ticks.