feat: release 1.1.0 with responsive task control

This commit is contained in:
chenmingxuan committed 2026-09-24 03:31:17 +08:00
1 parent 057f0c3050
commit bfcd828fa8
37 files changed
+1601 -151

No files matched your search

+57 -11
View File
@@ -1,6 +1,7 @@
"""Cooperative, session-scoped task guidance and device lease parking."""
import asyncio
from collections.abc import Awaitable
import json
import logging
import os
@@ -48,22 +49,39 @@ def request_resume(session_id: str) -> None:
def add_instruction(
session_id: str, instruction: str, *, release_loop: bool = False, source: str = "user"
) -> None:
) -> dict:
if not instruction.strip() or len(instruction) > 10000:
raise ValueError("指导内容必须在 1–10000 字之间")
directory = control_dir(session_id) / "instructions"
directory.mkdir(parents=True, exist_ok=True)
# Unique names make simultaneous web and MCP submissions additive, never last-write-wins.
path = directory / f"{time.time_ns():020d}-{uuid.uuid4().hex}.json"
_write_atomic(
path,
{
"instruction": instruction.strip(),
"release_loop": bool(release_loop),
"source": source,
"timestamp": time.time(),
},
)
record = {
"id": path.stem,
"instruction": instruction.strip(),
"release_loop": bool(release_loop),
"source": source,
"timestamp": time.time(),
}
_write_atomic(path, record)
return record
def list_instructions(session_id: str) -> list[dict]:
"""Show submissions separately from guidance the runner actually consumed."""
directory = control_dir(session_id) / "instructions"
records = []
for folder, status in ((directory, "pending"), (directory / "consumed", "consumed")):
for path in folder.glob("*.json") if folder.exists() else []:
try:
record = json.loads(path.read_text(encoding="utf-8"))
if isinstance(record, dict) and record.get("instruction"):
records.append(
{**record, "id": record.get("id") or path.stem, "status": status}
)
except (OSError, ValueError):
continue
return sorted(records, key=lambda record: (record.get("timestamp", 0), record["id"]))
def consume_instructions(base_dir: str | Path) -> dict | None:
@@ -73,8 +91,11 @@ def consume_instructions(base_dir: str | Path) -> dict | None:
try:
claimed = path.with_suffix(".reading")
path.rename(claimed)
found.append(json.loads(claimed.read_text(encoding="utf-8")))
record = json.loads(claimed.read_text(encoding="utf-8"))
record["consumed_at"] = time.time()
_write_atomic(directory / "consumed" / path.name, record)
claimed.unlink()
found.append(record)
except (OSError, ValueError):
continue
legacy = Path(base_dir) / "injected_instruction.json"
@@ -151,6 +172,31 @@ class TaskControl:
self._state("running")
return True
async def run_interruptibly[T](
self, operation: Awaitable[T], *, on_pause=None, on_resume=None
) -> tuple[bool, T | None]:
"""Park a task during a long model/helper call, then discard its stale result.
Only use for operations whose cancellation cannot leave a device action
half executed. The caller must return to fresh perception after resume.
"""
running = asyncio.ensure_future(operation)
pause = self.directory / "pause.request"
try:
while True:
if pause.exists():
running.cancel()
await asyncio.gather(running, return_exceptions=True)
await self.checkpoint(on_pause=on_pause, on_resume=on_resume)
return True, None
if running.done():
return False, running.result()
await asyncio.wait({running}, timeout=0.2)
finally:
if not running.done():
running.cancel()
await asyncio.gather(running, return_exceptions=True)
def release(self) -> None:
self.device_lock.release()
(self.directory / "task_control_state.json").unlink(missing_ok=True)