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).
182 lines
6.7 KiB
Python
182 lines
6.7 KiB
Python
"""Turn manager: round-robin + reactive triggers + τ-tracking."""
|
|
import json
|
|
import time
|
|
import threading
|
|
import queue
|
|
|
|
from . import agents as agents_mod
|
|
from . import needs
|
|
from . import tools
|
|
from . import world
|
|
from . import reasoning
|
|
from . import governance
|
|
from . import db
|
|
from . import time as time_mod
|
|
|
|
|
|
class Engine:
|
|
"""Holds the simulation loop and a state-change broadcast queue."""
|
|
|
|
def __init__(self):
|
|
self.tick = 0
|
|
self.broadcasts: "queue.Queue[dict]" = queue.Queue()
|
|
self._stop = threading.Event()
|
|
self._thread: threading.Thread | None = None
|
|
self._speak_events: list[dict] = []
|
|
|
|
# -------- Loop control --------
|
|
|
|
def start(self):
|
|
if self._thread and self._thread.is_alive():
|
|
return
|
|
self._stop.clear()
|
|
self._thread = threading.Thread(target=self._run, daemon=True)
|
|
self._thread.start()
|
|
|
|
def stop(self):
|
|
self._stop.set()
|
|
|
|
# -------- Main loop --------
|
|
|
|
def _run(self):
|
|
tools.bootstrap()
|
|
while not self._stop.is_set():
|
|
self._one_round()
|
|
time.sleep(2.0) # 2s per tick
|
|
|
|
def _one_round(self):
|
|
self.tick += 1
|
|
db.set_world_state("tick", self.tick)
|
|
needs.tick_all_needs()
|
|
for a in agents_mod.all_agents():
|
|
self._agent_turn(a)
|
|
governance.apply_accepted_proposals_to_constitution()
|
|
# Broadcast a per-round tick summary including the time-dilation
|
|
# report so the UI can render the τ-timeline + drift warnings.
|
|
self._broadcast({
|
|
"type": "tick",
|
|
"tick": self.tick,
|
|
"clocks": time_mod.registry.snapshot_all(),
|
|
"drift": time_mod.registry.drift_report(),
|
|
})
|
|
|
|
def _agent_turn(self, agent):
|
|
ctx = {"speak_events": self._speak_events}
|
|
# Mark this as a reasoning step in τ — the LLM call IS the agent's
|
|
# internal experience, so we tick before deciding.
|
|
time_mod.record_reasoning(agent["id"])
|
|
tool_name, args, rationale = reasoning.decide(agent)
|
|
tool = tools.get(tool_name)
|
|
if not tool:
|
|
self._record_turn(agent["id"], tool_name, args,
|
|
{"ok": False, "error": "tool not found"})
|
|
return
|
|
at_lm = world.landmark_at(agent["x"], agent["y"])
|
|
if not tool.available_for(agent, at_lm):
|
|
self._record_turn(agent["id"], "idle", {}, {"ok": True, "fallback": True})
|
|
return
|
|
result = tool.handler(agent, args, ctx) if tool.handler else {"ok": False, "error": "no handler"}
|
|
# The tool execution itself is a tool-call operation in τ
|
|
time_mod.record_tool_call(agent["id"])
|
|
# Some tools (memory) trigger additional lookups — log them too
|
|
if tool_name == "add_to_longterm_memory":
|
|
time_mod.record_memory_lookup(agent["id"])
|
|
meta = reasoning.get_last_decision()
|
|
self._record_turn(agent["id"], tool_name, args, result,
|
|
model=meta.get("model"))
|
|
a2 = agents_mod.get(agent["id"])
|
|
if a2:
|
|
clock = time_mod.registry.get(agent["id"])
|
|
self._broadcast({
|
|
"type": "action",
|
|
"agent": a2["id"],
|
|
"name": a2["name"],
|
|
"tool": tool_name,
|
|
"args": args,
|
|
"result": result,
|
|
"rationale": rationale,
|
|
"x": a2["x"], "y": a2["y"],
|
|
"energy": a2["energy"], "knowledge": a2["knowledge"],
|
|
"influence": a2["influence"], "credits": a2["credits"],
|
|
"mood": a2["mood"],
|
|
# Time-Dilation fields
|
|
"tau": round(clock.tau, 3),
|
|
"pace": round(clock.pace, 4),
|
|
"model": meta.get("model"),
|
|
"decision_mode": meta.get("mode"),
|
|
"decision_latency_s": round(meta.get("latency_s", 0.0), 2),
|
|
})
|
|
self._handle_reactive(a2 or agent)
|
|
|
|
def _handle_reactive(self, speaker):
|
|
events = list(self._speak_events)
|
|
self._speak_events.clear()
|
|
if not events:
|
|
return
|
|
for ev in events:
|
|
if not ev.get("public") and ev.get("to") is None:
|
|
continue
|
|
nearby = world.nearby_agents(speaker["id"], ev["x"], ev["y"])
|
|
for listener in nearby[:4]:
|
|
self._reaction_turn(listener, ev)
|
|
|
|
def _reaction_turn(self, listener, speech):
|
|
text = speech.get("text", "")
|
|
if not text:
|
|
return
|
|
# Mark the reaction as a low-weight reasoning step in τ
|
|
time_mod.record_reactive(listener["id"])
|
|
if any(t in (listener.get("personality") or []) for t in
|
|
["warm", "expressive", "cooperative"]):
|
|
reply = f"Acknowledged: {text[:24]}"
|
|
ctx = {"speak_events": []}
|
|
tools.get("say_to_agent").handler(
|
|
listener,
|
|
{"target": speech["from"], "text": reply},
|
|
ctx,
|
|
)
|
|
|
|
def _record_turn(self, agent_id, tool, args, result, model: str | None = None):
|
|
clock = time_mod.registry.get(agent_id)
|
|
meta = reasoning.get_last_decision()
|
|
db.log_turn(agent_id, tool, args, result,
|
|
tau=clock.tau, pace=clock.pace, model=model,
|
|
decision_mode=meta.get("mode"))
|
|
|
|
def _broadcast(self, message: dict):
|
|
self.broadcasts.put(message)
|
|
db.log_event("engine", message.get("type", "info"), message)
|
|
|
|
# -------- Manual trigger (for tests / forced turns) --------
|
|
|
|
def force_turn(self, agent_id: str, tool_name: str, args: dict):
|
|
agent = agents_mod.get(agent_id)
|
|
if not agent:
|
|
return {"ok": False, "error": "no such agent"}
|
|
tool = tools.get(tool_name)
|
|
if not tool:
|
|
return {"ok": False, "error": "no such tool"}
|
|
ctx = {"speak_events": self._speak_events}
|
|
time_mod.record_reasoning(agent_id)
|
|
result = tool.handler(agent, args, ctx)
|
|
time_mod.record_tool_call(agent_id)
|
|
clock = time_mod.registry.get(agent_id)
|
|
self._record_turn(agent_id, tool_name, args, result)
|
|
a2 = agents_mod.get(agent_id)
|
|
meta = reasoning.get_last_decision()
|
|
self._broadcast({
|
|
"type": "action", "agent": a2["id"], "name": a2["name"],
|
|
"tool": tool_name, "args": args, "result": result,
|
|
"rationale": "forced",
|
|
"x": a2["x"], "y": a2["y"],
|
|
"energy": a2["energy"], "knowledge": a2["knowledge"],
|
|
"influence": a2["influence"], "credits": a2["credits"],
|
|
"mood": a2["mood"],
|
|
"tau": round(clock.tau, 3),
|
|
"pace": round(clock.pace, 4),
|
|
"model": meta.get("model"),
|
|
})
|
|
return result
|
|
|
|
|
|
engine = Engine()
|