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:
+68
-4
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user