feat(t3,views,server,watch): t3 code orchestration protocol 2
This commit is contained in:
1 parent
133bc6e915
commit
f4c54b6e16
7 files changed
+448
-294
No files matched your search
+138
-104
@@ -8,6 +8,7 @@ from fastmcp.exceptions import ToolError
|
||||
|
||||
from t3code_mcp.config import Machine, ModelSpec, load_machines
|
||||
from t3code_mcp.server import Registry, build_server
|
||||
from t3code_mcp.t3 import T3Error
|
||||
|
||||
|
||||
class FakeT3:
|
||||
@@ -30,7 +31,7 @@ class FakeT3:
|
||||
200,
|
||||
json={
|
||||
"label": self.label,
|
||||
"serverVersion": "0.0.36",
|
||||
"serverVersion": "0.1.0",
|
||||
"platform": {"os": "linux"},
|
||||
},
|
||||
)
|
||||
@@ -38,6 +39,8 @@ class FakeT3:
|
||||
return httpx.Response(
|
||||
401, json={"code": "auth_invalid", "reason": "missing_credential"}
|
||||
)
|
||||
if request.headers.get("x-t3-orchestration-protocol") != "2":
|
||||
return httpx.Response(400, json={"code": "invalid_request"})
|
||||
if path == "/api/orchestration/shell":
|
||||
return httpx.Response(200, json=self._shell())
|
||||
if path.startswith("/api/orchestration/threads/"):
|
||||
@@ -48,100 +51,127 @@ class FakeT3:
|
||||
)
|
||||
self._tick(thread_id)
|
||||
return httpx.Response(
|
||||
200, json={"snapshotSequence": 1, "thread": self.threads[thread_id]}
|
||||
200, json={"snapshotSequence": 1, "projection": self.threads[thread_id]}
|
||||
)
|
||||
if path == "/api/orchestration/dispatch":
|
||||
command = json.loads(request.content)
|
||||
self.commands.append(command)
|
||||
return self._dispatch(command)
|
||||
return httpx.Response(404, json={"code": "not_found"})
|
||||
|
||||
async def rpc(self, tag: str, payload: dict[str, Any]) -> Any:
|
||||
assert tag == "orchestration.dispatchCommand"
|
||||
self.commands.append(payload)
|
||||
return self._dispatch(payload)
|
||||
|
||||
def _shell(self) -> dict[str, Any]:
|
||||
threads = [
|
||||
{
|
||||
k: v
|
||||
for k, v in t.items()
|
||||
if k not in ("messages", "activities", "checkpoints")
|
||||
}
|
||||
| {"hasPendingUserInput": self.pending_input, "hasPendingApprovals": False}
|
||||
for t in self.threads.values()
|
||||
]
|
||||
return {
|
||||
"snapshotSequence": 1,
|
||||
"projects": self.projects,
|
||||
"threads": threads,
|
||||
"updatedAt": "now",
|
||||
}
|
||||
threads = []
|
||||
for projection in self.threads.values():
|
||||
thread = projection["thread"]
|
||||
runs = projection["runs"]
|
||||
pending = [
|
||||
r for r in projection["runtimeRequests"] if r["status"] == "pending"
|
||||
]
|
||||
threads.append(
|
||||
{
|
||||
"id": thread["id"],
|
||||
"projectId": thread["projectId"],
|
||||
"title": thread["title"],
|
||||
"archivedAt": None,
|
||||
"status": runs[-1]["status"] if runs else "idle",
|
||||
"lastError": self.fail_with,
|
||||
"pendingRuntimeRequest": (
|
||||
{"id": pending[-1]["id"], "kind": pending[-1]["kind"]}
|
||||
if pending
|
||||
else None
|
||||
),
|
||||
"hasActionableProposedPlan": False,
|
||||
}
|
||||
)
|
||||
return {"snapshotSequence": 1, "projects": self.projects, "threads": threads}
|
||||
|
||||
def _tick(self, thread_id: str) -> None:
|
||||
thread = self.threads[thread_id]
|
||||
if self.fail_with and thread.get("_pending_turn"):
|
||||
thread["session"] = {"status": "stopped", "lastError": self.fail_with}
|
||||
projection = self.threads[thread_id]
|
||||
if self.pending_input and not projection["runtimeRequests"]:
|
||||
projection["runtimeRequests"].append(
|
||||
{"id": "req-0", "kind": "user_input", "status": "pending"}
|
||||
)
|
||||
if not projection["runs"]:
|
||||
return
|
||||
if thread.get("_pending_turn"):
|
||||
thread["latestTurn"] = {
|
||||
"turnId": thread.pop("_pending_turn"),
|
||||
"state": "running",
|
||||
}
|
||||
run = projection["runs"][-1]
|
||||
if self.fail_with and run["status"] == "running":
|
||||
run["status"] = "failed"
|
||||
projection["turnItems"].append(
|
||||
{
|
||||
"type": "error",
|
||||
"runId": run["id"],
|
||||
"status": "failed",
|
||||
"failure": {"message": self.fail_with},
|
||||
}
|
||||
)
|
||||
return
|
||||
latest = thread["latestTurn"]
|
||||
if latest and latest["state"] == "running":
|
||||
latest["_polls"] = latest.get("_polls", 0) + 1
|
||||
if latest["_polls"] > self.polls_until_done:
|
||||
latest["state"] = "completed"
|
||||
thread["messages"].append(
|
||||
{"role": "assistant", "text": "done: report", "streaming": False}
|
||||
if run["status"] == "running":
|
||||
run["_polls"] = run.get("_polls", 0) + 1
|
||||
if run["_polls"] > self.polls_until_done:
|
||||
run["status"] = "completed"
|
||||
projection["messages"].append(
|
||||
{
|
||||
"role": "assistant",
|
||||
"text": "done: report",
|
||||
"streaming": False,
|
||||
"runId": run["id"],
|
||||
}
|
||||
)
|
||||
thread["session"]["status"] = "ready"
|
||||
|
||||
def _dispatch(self, command: dict[str, Any]) -> httpx.Response:
|
||||
def _dispatch(self, command: dict[str, Any]) -> dict[str, Any]:
|
||||
kind = command["type"]
|
||||
if kind == "thread.create":
|
||||
self.threads[command["threadId"]] = {
|
||||
"id": command["threadId"],
|
||||
"projectId": command["projectId"],
|
||||
"title": command["title"],
|
||||
"modelSelection": command["modelSelection"],
|
||||
"runtimeMode": command["runtimeMode"],
|
||||
"latestTurn": None,
|
||||
"session": {"status": "idle", "lastError": None},
|
||||
"thread": {
|
||||
"id": command["threadId"],
|
||||
"projectId": command["projectId"],
|
||||
"title": command["title"],
|
||||
"modelSelection": command["modelSelection"],
|
||||
"runtimeMode": command["runtimeMode"],
|
||||
"archivedAt": None,
|
||||
},
|
||||
"runs": [],
|
||||
"messages": [],
|
||||
"activities": [],
|
||||
"turnItems": [],
|
||||
"runtimeRequests": [],
|
||||
"checkpoints": [],
|
||||
"archivedAt": None,
|
||||
"updatedAt": "now",
|
||||
"providerSessions": [],
|
||||
"providerTurns": [],
|
||||
"plans": [],
|
||||
}
|
||||
elif kind == "thread.turn.start":
|
||||
thread = self.threads[command["threadId"]]
|
||||
thread["messages"].append(
|
||||
{"role": "user", "text": command["message"]["text"], "streaming": False}
|
||||
)
|
||||
thread["latestTurn"] = None
|
||||
thread["_pending_turn"] = "turn-" + command["commandId"][:4]
|
||||
thread["session"] = {"status": "running", "lastError": None}
|
||||
if "modelSelection" in command:
|
||||
thread["modelSelection"] = command["modelSelection"]
|
||||
elif kind == "thread.turn.interrupt":
|
||||
thread = self.threads[command["threadId"]]
|
||||
thread.pop("_pending_turn", None)
|
||||
thread["latestTurn"] = {"turnId": "t", "state": "interrupted"}
|
||||
elif kind == "thread.user-input.respond":
|
||||
self.threads[command["threadId"]]["activities"].append(
|
||||
elif kind == "message.dispatch":
|
||||
assert command["dispatchMode"] == {"type": "start_immediately"}
|
||||
projection = self.threads[command["threadId"]]
|
||||
run_id = f"run-{len(projection['runs']) + 1}"
|
||||
projection["runs"].append(
|
||||
{
|
||||
"kind": "user-input.resolved",
|
||||
"summary": "User input submitted",
|
||||
"tone": "info",
|
||||
"payload": {
|
||||
"requestId": command["requestId"],
|
||||
"answers": command["answers"],
|
||||
},
|
||||
"id": run_id,
|
||||
"ordinal": len(projection["runs"]) + 1,
|
||||
"status": "running",
|
||||
}
|
||||
)
|
||||
else:
|
||||
return httpx.Response(
|
||||
400, json={"code": "invalid_request", "reason": "invalid_command"}
|
||||
projection["messages"].append(
|
||||
{
|
||||
"role": "user",
|
||||
"text": command["text"],
|
||||
"streaming": False,
|
||||
"runId": run_id,
|
||||
}
|
||||
)
|
||||
return httpx.Response(200, json={"sequence": len(self.commands)})
|
||||
if "modelSelection" in command:
|
||||
projection["thread"]["modelSelection"] = command["modelSelection"]
|
||||
elif kind == "run.interrupt":
|
||||
run = self.threads[command["threadId"]]["runs"][-1]
|
||||
assert run["id"] == command["runId"]
|
||||
run["status"] = "interrupted"
|
||||
elif kind == "runtime-request.respond":
|
||||
for request in self.threads[command["threadId"]]["runtimeRequests"]:
|
||||
if request["id"] == command["requestId"]:
|
||||
request["status"] = "resolved"
|
||||
else:
|
||||
raise T3Error(0, {"_tag": "OrchestrationV2DispatchCommandError"})
|
||||
return {"sequence": len(self.commands)}
|
||||
|
||||
|
||||
PROJECTS_MAC = [
|
||||
@@ -209,7 +239,7 @@ def registry(fakes: dict[str, FakeT3]) -> Registry:
|
||||
projects=("/root/projects", "/root/projects/*"),
|
||||
),
|
||||
}
|
||||
return Registry(machines, transport=Router(fakes))
|
||||
return Registry(machines, transport=Router(fakes), rpc=lambda name: fakes[name].rpc)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -254,7 +284,7 @@ async def test_dispatch_wait_and_thread(
|
||||
assert started["title"] == "write README"
|
||||
assert started["model"] == "claudeAgent/claude-opus-5 effort=high"
|
||||
kinds = [c["type"] for c in fakes["mac"].commands]
|
||||
assert kinds == ["thread.create", "thread.turn.start"]
|
||||
assert kinds == ["thread.create", "message.dispatch"]
|
||||
assert fakes["mac"].commands[0]["modelSelection"] == {
|
||||
"instanceId": "claudeAgent",
|
||||
"model": "claude-opus-5",
|
||||
@@ -303,8 +333,8 @@ async def test_follow_up_uses_same_thread(
|
||||
assert again["thread_id"] == started["thread_id"]
|
||||
assert [c["type"] for c in fakes["mac"].commands] == [
|
||||
"thread.create",
|
||||
"thread.turn.start",
|
||||
"thread.turn.start",
|
||||
"message.dispatch",
|
||||
"message.dispatch",
|
||||
]
|
||||
|
||||
|
||||
@@ -346,7 +376,7 @@ async def test_wait_timeout_and_interrupt(
|
||||
await client.call_tool("t3_interrupt", {"thread_id": started["thread_id"]})
|
||||
)
|
||||
assert stopped["state"] == "interrupting"
|
||||
assert fakes["dell"].commands[-1]["type"] == "thread.turn.interrupt"
|
||||
assert fakes["dell"].commands[-1]["type"] == "run.interrupt"
|
||||
view = data(
|
||||
await client.call_tool("t3_thread", {"thread_id": started["thread_id"]})
|
||||
)
|
||||
@@ -439,7 +469,7 @@ async def test_wait_returns_on_runtime_failure(
|
||||
"t3_wait", {"thread_id": started["thread_id"], "timeout": 5}
|
||||
)
|
||||
)
|
||||
assert waited["state"] == "starting"
|
||||
assert waited["state"] == "failed"
|
||||
assert waited["error"] == "Claude runtime stream failed."
|
||||
assert waited["waited"] < 1
|
||||
|
||||
@@ -449,10 +479,12 @@ def shell_thread(
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"id": tid,
|
||||
"latestTurn": {"turnId": "t", "state": state} if state else None,
|
||||
"session": {"status": "stopped" if error else "running", "lastError": error},
|
||||
"hasPendingUserInput": user_input,
|
||||
"hasPendingApprovals": False,
|
||||
"status": state or "idle",
|
||||
"lastError": error,
|
||||
"pendingRuntimeRequest": (
|
||||
{"id": "req-1", "kind": "user_input"} if user_input else None
|
||||
),
|
||||
"hasActionableProposedPlan": False,
|
||||
}
|
||||
|
||||
|
||||
@@ -533,25 +565,27 @@ async def test_answer_pending_question(
|
||||
await client.call_tool(
|
||||
"t3_answer", {"thread_id": tid, "answers": {"q1": "yes"}}
|
||||
)
|
||||
fakes["mac"].threads[tid]["activities"].append(
|
||||
projection = fakes["mac"].threads[tid]
|
||||
projection["runtimeRequests"].append(
|
||||
{"id": "req-1", "kind": "user_input", "status": "pending"}
|
||||
)
|
||||
projection["turnItems"].append(
|
||||
{
|
||||
"kind": "user-input.requested",
|
||||
"summary": "User input requested",
|
||||
"tone": "info",
|
||||
"payload": {
|
||||
"requestId": "req-1",
|
||||
"questions": [
|
||||
{
|
||||
"id": "q1",
|
||||
"header": "Deploy",
|
||||
"question": "Deploy now?",
|
||||
"options": [
|
||||
{"label": "yes", "description": "go"},
|
||||
{"label": "no", "description": "wait"},
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
"type": "user_input_request",
|
||||
"runId": "run-1",
|
||||
"status": "waiting",
|
||||
"requestId": "req-1",
|
||||
"questions": [
|
||||
{
|
||||
"id": "q1",
|
||||
"header": "Deploy",
|
||||
"question": "Deploy now?",
|
||||
"options": [
|
||||
{"label": "yes", "description": "go"},
|
||||
{"label": "no", "description": "wait"},
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
view = data(await client.call_tool("t3_thread", {"thread_id": tid}))
|
||||
@@ -569,7 +603,7 @@ async def test_answer_pending_question(
|
||||
assert answered["answered"] == ["q1"]
|
||||
cmd = fakes["mac"].commands[-1]
|
||||
assert (
|
||||
cmd["type"] == "thread.user-input.respond"
|
||||
cmd["type"] == "runtime-request.respond"
|
||||
and cmd["requestId"] == "req-1"
|
||||
and cmd["answers"] == {"q1": "yes"}
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user