feat(watch,server): t3_watch to turn thread events off and back on

This commit is contained in:
hh committed 2026-09-01 13:37:04 +00:00
1 parent a98211b79f
commit a29c526ee9
5 files changed
+159 -11

No files matched your search

+57 -3
View File
@@ -23,7 +23,7 @@ from t3code_mcp import t3, views
from t3code_mcp.config import Machine, ModelSpec
from t3code_mcp.t3 import T3Client, T3Error
from t3code_mcp.views import BUSY
from t3code_mcp.watch import Tracker
from t3code_mcp.watch import Tracked, Tracker
if TYPE_CHECKING:
from collections.abc import AsyncIterator, Callable, Coroutine
@@ -121,6 +121,16 @@ def _project_view(
}
def _watch_view(thread_id: str, tracked: Tracked) -> dict[str, Any]:
return {
"thread_id": thread_id,
"machine": tracked.machine,
"project": tracked.project,
"title": tracked.title,
"state": tracked.state,
}
def build_server(
registry: Registry,
*,
@@ -145,7 +155,8 @@ def build_server(
"Coding threads on Бобёр's machines through T3 Code. Flow: t3_machines → "
"t3_projects(machine) → t3_dispatch, then either t3_wait (block) or just "
"carry on: the gateway injects an event when a dispatched thread finishes, "
"fails or asks a question (answer with t3_answer). Prompts run "
"fails or asks a question (answer with t3_answer; t3_watch turns those "
"events off for a thread). Prompts run "
"autonomously (full-access, no approvals): say what to change, what to "
"check, what not to touch, and that the answer should end with a report."
),
@@ -222,6 +233,15 @@ def build_server(
str | None,
Field(description="Continue this existing thread instead of creating one"),
] = None,
watch: Annotated[ # noqa: FBT002 - MCP arguments are always named
bool,
Field(
description=(
"Report this thread back as an event; false for a thread you "
"read yourself. Every dispatch resets the flag - see t3_watch"
)
),
] = True,
) -> dict[str, Any]:
"""Start a coding thread on a machine (or follow up in one); returns at once.
@@ -269,7 +289,7 @@ def build_server(
)
await _call(client.dispatch(t3.turn_start(tid, text, title_seed=heading)))
registry.threads[tid] = machine
registry.tracker.track(tid, machine, target["title"], heading)
registry.tracker.track(tid, machine, target["title"], heading, watch=watch)
return {
"thread_id": tid,
"machine": machine,
@@ -278,6 +298,40 @@ def build_server(
"title": heading,
"model": str(selection),
"state": "running",
"watch": watch,
}
@mcp.tool
async def t3_watch(
thread_id: Annotated[
str | None,
Field(description="Thread to subscribe or unsubscribe; omit to only list"),
] = None,
enabled: Annotated[
bool | None,
Field(description="True - report the thread back again, false - stop"),
] = None,
) -> dict[str, Any]:
"""Which dispatched threads report back, and turn that on or off per thread.
t3_dispatch subscribes its thread, so its completion, failure and
questions arrive here as events. Unsubscribe a thread someone reads
directly: it keeps running, it just stops reporting. Called with no
arguments this only lists.
"""
if thread_id is not None:
if enabled is None:
msg = "pass enabled=true or enabled=false along with thread_id"
raise ToolError(msg)
try:
registry.tracker.set_watch(thread_id, enabled=enabled)
except KeyError:
msg = f"thread {thread_id} was not dispatched from here; nothing to "
raise ToolError(msg + ("watch" if enabled else "unwatch")) from None
threads = registry.tracker.threads
return {
"watched": [_watch_view(k, v) for k, v in threads.items() if v.watch],
"unwatched": [_watch_view(k, v) for k, v in threads.items() if not v.watch],
}
@mcp.tool