feat(backend): full remote implementation, untested

This commit is contained in:
Shivering Ocelot committed 2026-09-02 10:46:51 -05:00
1 parent 0705f4abc5
commit 5009a4eb4c
8 files changed
+485 -33

No files matched your search

+67 -9
View File
@@ -19,6 +19,7 @@ import agents as agents_mod
import temporary as tmp
from server import start_server
from chat import ChatView, Message, MESSAGE_PAGE_SIZE
from remote import RemoteConnection, RemoteToolSpec, parse_function_specs
from app_mixins import ChatMixin
@@ -26,7 +27,8 @@ class App(ChatMixin):
def __init__(self, data_directory, replicate_token=None, openrouter_token=None):
self.data_directory = data_directory
self.user_connections = {}
self.remote_connections = {}
# Connected remotes: (user_id, remotename) -> RemoteConnection.
self.remotes = {}
self.streams = {}
self.event_listeners = []
# active websocket tasks, for manual cancelling upon server shutdown
@@ -81,15 +83,71 @@ class App(ChatMixin):
if not self.user_connections[user_id]:
del self.user_connections[user_id]
def add_remote_connection(self, user_id, remotename, channel):
if user_id not in self.remote_connections:
self.remote_connections[user_id] = {}
user_connections = self.remote_connections[user_id]
# --- Remotes ---
if remotename not in user_connections:
user_connections[remotename] = []
if channel not in user_connections[remotename]:
user_connections[remotename].append(channel)
def connectRemote(self, user_id, name, function_specs, send):
"""Connect a remote served by a live session's send channel.
Remotes are scoped to the user: connecting a name that is already
connected fails (no overriding). The remote stays connected until
disconnectRemote — the 'remote-disconnect' action or the close of
the serving session."""
if not name:
raise Exception("remotename is required")
if (user_id, name) in self.remotes:
raise Exception(f"remote '{name}' is already connected")
connection = RemoteConnection(
user_id, name, parse_function_specs(function_specs), send)
self.remotes[(user_id, name)] = connection
self.emit_event("remote-connected", {"remotename": name, "remote": connection.describe()})
return connection
def disconnectRemote(self, user_id, name):
"""Disconnect a remote of this user; its pending calls fail.
Idempotent: returns the connection, or None when no remote is
connected under that name (used by both the manual disconnect
action and session teardown)."""
connection = self.remotes.pop((user_id, name), None)
if connection is not None:
connection.close()
self.emit_event("remote-disconnected", {"remotename": name})
return connection
def listRemotes(self, user_id):
"""All connected remotes of this user."""
return [conn.describe()
for key, conn in self.remotes.items() if key[0] == user_id]
async def callRemote(self, user_id, name, function, args):
"""Invoke a function of a connected remote and await the result.
Raises on: unknown remote, unknown function, timeout, or an error
reported by the remote."""
connection = self.remotes.get((user_id, name))
if connection is None:
raise Exception(f"no connected remote named '{name}'")
return await connection.call(function, args)
def getRemoteTools(self, user_id, remote_specs):
"""Expand run-input remote specs into agent tools.
Entries are remote names, or {'name': ..., 'prefix': ...} to prefix
the tool names. Every function of the remote becomes one tool named
after the function itself, prefixed only when requested. Remotes
that are not connected are skipped with a warning: they can vanish
at any time and the run should proceed without them.
"""
tools = []
for spec in remote_specs or []:
if isinstance(spec, str):
name, prefix = spec, ''
else:
name, prefix = spec.get('name'), spec.get('prefix', '')
connection = self.remotes.get((user_id, name))
if connection is None:
print(f"WARNING: remote '{name}' requested by run input is not connected, skipping")
continue
for fn in connection.functions.values():
tools.append(RemoteToolSpec(connection, fn, name=prefix + fn.name))
return tools
# --- Agent Methods (Asynchronous) ---