-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.py
More file actions
108 lines (86 loc) · 3.2 KB
/
Copy pathserver.py
File metadata and controls
108 lines (86 loc) · 3.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import asyncio
import logging
from contextlib import asynccontextmanager
from dbos import DBOS
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
from sqlalchemy import text
from harness.bus import emit, history, subscribe
from harness.code_mode import call
from harness.db import db_client, ensure_schema
from harness.runtime import agent_workflow
from harness.supervisor import supervisor_workflow
logger = logging.getLogger("harness.rpc")
@asynccontextmanager
async def lifespan(app: FastAPI):
ensure_schema()
DBOS.launch()
yield
DBOS.destroy()
app = FastAPI(lifespan=lifespan)
# Keep references so background runs aren't garbage-collected mid-execution.
_running_tasks: set[asyncio.Task] = set()
async def run_task(task: str, mode: str = "default") -> None:
workflow_id = ""
try:
if mode == "supervised":
handle = await DBOS.start_workflow_async(supervisor_workflow, task)
else:
handle = await DBOS.start_workflow_async(agent_workflow, task)
workflow_id = handle.workflow_id
await handle.get_result()
except Exception as e:
emit({"type": "workflow.failed", "workflowId": workflow_id, "error": str(e)})
@app.post("/rpc/tool")
async def rpc_tool(req: Request):
try:
body = await req.json()
return {"result": call(body["token"], body["name"], body["args"])}
except PermissionError as e:
return {"error": str(e)}
except Exception:
# Return generic message (no internals)
logger.exception("rpc_tool failed")
return {"error": "internal error"}
@app.websocket("/ws")
async def ws(websocket: WebSocket):
# 1. Accept the browser's connection.
await websocket.accept()
# 2. Bridge the sync bus to the async socket
queue: asyncio.Queue = asyncio.Queue()
unsub = subscribe(lambda e: queue.put_nowait(e))
# 3. Replay the whole timeline from Postgres
for event in history():
await websocket.send_json(event)
# 4. Push queued events to the browser, forever
async def forward():
try:
while True:
event = await queue.get()
await websocket.send_json(event)
except Exception:
pass
forward_task = asyncio.create_task(forward())
# 5. Receive commands from the browser (browser -> agent).
try:
while True:
msg = await websocket.receive_json()
if msg.get("type") == "submit_task":
task = msg.get("input")
if task:
mode = msg.get("mode", "default")
# Run the agent in the background (agent -> queue), keeping a
# reference so the task isn't garbage-collected mid-run.
t = asyncio.create_task(run_task(task, mode))
_running_tasks.add(t)
t.add_done_callback(_running_tasks.discard)
except WebSocketDisconnect:
pass
finally:
# 6. Clean up when the browser leaves.
forward_task.cancel()
unsub()
@app.post("/reset")
def reset():
with db_client.begin() as conn:
conn.execute(text("TRUNCATE TABLE event_log RESTART IDENTITY"))
return {"ok": True}