movement: smooth animated moves (move duration, observable intermediate positions) + proactive cruise (glide while the LLM thinks, collision-safe, reported back); verified mission with gpt-5.6-luna

This commit is contained in:
opencode
2026-08-08 21:36:32 +03:00
parent 1cb3bdc46e
commit 315762363e
6 changed files with 201 additions and 11 deletions
+68 -4
View File
@@ -9,6 +9,7 @@ tool calls/results fed back as `tool` messages.
from __future__ import annotations
import asyncio
import base64
import io
import json
@@ -463,10 +464,12 @@ class LLMController:
async def send_user(self, text: str) -> None:
self.messages.append({"role": "user", "content": text})
async def invoke(self) -> TurnResult:
"""One model round-trip: get the response and execute its tool calls."""
async def request(self) -> Any:
"""Send the current conversation to the model and return the raw
response WITHOUT executing any tool calls. Lets a caller overlap the
model's thinking time with other work (e.g. proactive cruising)."""
try:
resp = await self.ac.chat.completions.create(
return await self.ac.chat.completions.create(
model=self.model,
messages=self.messages,
tools=self.tools,
@@ -476,6 +479,12 @@ class LLMController:
self.log("agent", f"LLM error: {exc}")
raise RuntimeError(f"LLM error: {exc}") from exc
async def invoke(self) -> TurnResult:
"""One model round-trip: get the response and execute its tool calls."""
return await self.execute(await self.request())
async def execute(self, resp: Any) -> TurnResult:
"""Execute the tool calls of a previously requested response."""
choice = resp.choices[0]
text = choice.message.content or ""
result = TurnResult(text=text)
@@ -596,11 +605,15 @@ async def run_llm_agent_loop(
recorder: Any | None = None,
nudge_limit: int = 1,
look_every: int = 0,
cruise: float = 0.0,
) -> dict[str, Any]:
"""Drive the controller until the mission is done or steps run out.
``look_every``: attach a fresh camera frame every N steps so the model
always sees recent visual context without asking (0 disables).
``cruise``: proactive motion — while the model is thinking, the capsule
keeps gliding forward (meters per think, 0 disables). Collisions stop the
drift and are reported to the model.
"""
summary: dict[str, Any] = {
"steps": 0,
@@ -613,6 +626,45 @@ async def run_llm_agent_loop(
last_correct_step = -99
last_collision_step = -99
async def drift(amount: float, step: int) -> None:
"""Low-level controller: keep moving forward while the brain thinks."""
nonlocal last_collision_step
remaining = amount
total = 0.0
while remaining > 0.02:
mv = await controller.client.call_tool(
"move", {"forward": min(0.4, remaining), "duration": 0.5}
)
out = mv.output
controller.observe(out)
moved = out.get("moved", 0.0)
total += moved
remaining -= moved
if out.get("collision"):
last_collision_step = step
controller.messages.append(
{
"role": "user",
"content": (
"While you were thinking, the capsule drifted forward and bumped "
f"into {out.get('collision_normal')}. It stopped. Navigate around it."
),
}
)
log("agent", "(cruise: bumped while thinking — stopped)")
return
if total > 0.05:
controller.messages.append(
{
"role": "user",
"content": (
f"While you were thinking, the capsule kept moving (auto-cruise, "
f"{total:.2f} m forward). {controller.state_hint()}"
),
}
)
log("agent", f"(cruise: drifted {total:.2f} m while thinking)")
def maybe_correct(step: int) -> None:
nonlocal corrections, last_correct_step
if controller.pos is None or controller.beacon_pos is None:
@@ -652,6 +704,7 @@ async def run_llm_agent_loop(
log("agent", "(correction: re-aim at the beacon)")
dist_history.clear()
pending_resp: Any | None = None # LLM response requested while we cruised
for step in range(max_steps):
summary["steps"] = step + 1
if recorder is not None and controller.pos is not None:
@@ -660,7 +713,12 @@ async def run_llm_agent_loop(
# sees what is happening without having to ask.
if look_every and step % look_every == 0:
await controller.auto_frame()
turn = await controller.invoke()
if pending_resp is not None:
resp = await pending_resp
pending_resp = None
turn = await controller.execute(resp)
else:
turn = await controller.invoke()
if turn.text:
log("agent", turn.text[:400])
summary["tool_calls"] += len(turn.calls)
@@ -706,5 +764,11 @@ async def run_llm_agent_loop(
return summary
controller.messages.append({"role": "user", "content": NUDGE})
log("agent", "(nudge: no tool call; continue the mission)")
continue
# Proactive motion: request the next response and, while the model
# thinks, keep the capsule gliding forward (collision-safe).
if cruise > 0.0 and step < max_steps - 1:
pending_resp = asyncio.create_task(controller.request())
await drift(cruise, step)
summary["result"] = f"exceeded {max_steps} steps"
return summary