bugfix: small fixes
This commit is contained in:
1 parent
30afcf2803
commit
d9204d262c
4 files changed
+64
-91
No files matched your search
@@ -38,6 +38,7 @@ class ExpertAgent(Agent):
|
|||||||
# Inject remotes as tools
|
# Inject remotes as tools
|
||||||
if 'remotes' in run.input:
|
if 'remotes' in run.input:
|
||||||
tools += self.app.getRemoteTools(self.user_id, run.input['remotes'])
|
tools += self.app.getRemoteTools(self.user_id, run.input['remotes'])
|
||||||
|
using_agent_tools = True
|
||||||
|
|
||||||
print("Final tool object: " + repr(tools))
|
print("Final tool object: " + repr(tools))
|
||||||
if tools: run.input['tools'] = tools
|
if tools: run.input['tools'] = tools
|
||||||
|
|||||||
@@ -350,8 +350,6 @@ async def openrouter_chat_completion(app, messages, model, tools=[], opts={}, ag
|
|||||||
if formatted_tools:
|
if formatted_tools:
|
||||||
kwargs['tools'] = formatted_tools
|
kwargs['tools'] = formatted_tools
|
||||||
|
|
||||||
print("all kwargs: " + repr(kwargs))
|
|
||||||
|
|
||||||
stream_response = await app.openrouter_client.chat.send_async(**kwargs)
|
stream_response = await app.openrouter_client.chat.send_async(**kwargs)
|
||||||
|
|
||||||
# OpenAI format streams tool calls in parts
|
# OpenAI format streams tool calls in parts
|
||||||
|
|||||||
+61
-89
@@ -2,114 +2,86 @@
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
A **session** is one authenticated `/ws` connection. Authentication is
|
A **remote** is a collection of functions exposed to an AI agent through the WebSocket session. Remotes are scoped to a user and live in memory during the server's lifetime.
|
||||||
uniform — token or username/password — and every session is a user
|
|
||||||
connection that receives event broadcasts and can send any action.
|
|
||||||
|
|
||||||
A **remote** is a named set of functions served by a session:
|
## Architecture & Function Lifecycle
|
||||||
|
|
||||||
- Connected by that session with the `remote-connect` action (backed by the
|
Remote functions are not standard Python methods attached to the agent. Instead, they are definitions (schemas) wrapped into adapters that fit the expert agent's tool loop.
|
||||||
`App.connectRemote` method), stored in `App.remotes` scoped to the user.
|
|
||||||
- **Lifetime**: connected until `remote-disconnect` (manual, allowed from any
|
|
||||||
session of the same user) or until the serving session closes — session
|
|
||||||
close disconnects every remote it connected. Nothing is persisted.
|
|
||||||
- **No overriding**: connecting a name that is already connected for that
|
|
||||||
user fails with an error reply.
|
|
||||||
|
|
||||||
**Local Remotes** — purely user-side constructs, entirely implemented and
|
1. **Definition (`backend/src/remote.py`)**:
|
||||||
stored in the frontend (`Remotes.vue`). They never touch the backend.
|
* A **`RemoteFunction`** represents a function exposed by a remote. It contains the function's name, description, and its OpenAI-compatible schema for input parameters.
|
||||||
|
* A **`RemoteToolSpec`** wraps a `RemoteFunction` to make it compatible with the agent's tool system. It exposes the required interface: `.name`, `.description`, `.to_openai_dict()` (for the LLM), and `.execute_async(**args)` (for execution).
|
||||||
|
* A **`RemoteConnection`** manages the connection state. It holds the function definitions and an internal queue of pending calls.
|
||||||
|
|
||||||
Agents never link to remotes. A prompt passes remote names in
|
2. **Injection**:
|
||||||
`options.remotes`; `ExpertAgent.run` expands them into run-scoped tools.
|
* When a run starts (see **Agent Integration** below), the expert agent requests tools for specific remotes. The server resolves these remote names into `RemoteToolSpec` objects and adds them to the run's input tools list.
|
||||||
Every function of a remote becomes a tool named exactly after the function,
|
|
||||||
prefixed only when the run input asks for it.
|
|
||||||
|
|
||||||
This also implements the existing `server.py` TODO ("remotes aren't separate
|
3. **Execution**:
|
||||||
connections — any connection can create a remotes and listen for requests
|
* When the AI decides to use a remote tool, the expert agent's loop calls `execute_async` on the `RemoteToolSpec`.
|
||||||
sent to them").
|
* This triggers `RemoteConnection.call()`, which sends a request over the WebSocket to the session that owns the remote.
|
||||||
|
* The remote executes the function code (for local remotes) or accepts an RPC call (for external remotes).
|
||||||
|
* Results are sent back through `remote-call-result` actions. Pending calls time out after 30 seconds if no response is received.
|
||||||
|
|
||||||
The frontend is already implemented and out of scope; the backend serves the
|
4. **Lifecycle**:
|
||||||
two actions it uses (`remote-list`, `remote-call`) with the shapes below.
|
* Remotes are established when a user connects them via the UI (`remote-connect`).
|
||||||
|
* They persist for the session lifetime unless manually disconnected (`remote-disconnect`) or if the session serving them closes.
|
||||||
|
* Collisions are handled by scoping connections to `(user_id, name)`.
|
||||||
|
|
||||||
## Wire protocol
|
## API Reference
|
||||||
|
|
||||||
Every message in both directions uses the existing action envelope. There is
|
All endpoints are actions sent to the backend via the WebSocket.
|
||||||
no separate `type`-keyed protocol.
|
|
||||||
|
|
||||||
Upstream (session → backend):
|
### `remote-connect`
|
||||||
|
Registers a new remote connection for the current user.
|
||||||
|
|
||||||
| action | payload | success reply | error reply |
|
**Payload**:
|
||||||
|---|---|---|---|
|
* `remotename` (string): The unique name for this remote collection.
|
||||||
| `remote-connect` | body `{remotename, tools}` | `{'message': 'remote connected', 'remotename', 'nonce'}` | `{'error': "remote 'x' is already connected", 'nonce'}` |
|
* `functions` (array of objects): List of function specs. Each object must contain:
|
||||||
| `remote-disconnect` | body `{remotename}` | `{'message': 'remote disconnected', 'remotename', 'nonce'}` | `{'error': "no connected remote named 'x'", 'nonce'}` |
|
* `name` (string): The function identifier.
|
||||||
| `remote-list` | — | `{'remotes': [...], 'nonce'}` | — |
|
* `description` (string): Text visible to the AI.
|
||||||
| `remote-call` | body `{remotename, function, args}` | `{'result': <JSON value>, 'nonce'}` | `{'error': '...', 'nonce'}` |
|
* `parameters` (object): OpenAI tool parameter schema (`type`, `properties`, `required`).
|
||||||
| `remote-call-result` | top-level `{requestId, result}` or `{requestId, error}` | none | none |
|
|
||||||
|
|
||||||
`tools` entries (function specs) and the `remote-list` entries share one
|
### `remote-disconnect`
|
||||||
shape: `{name, description, parameters}` with `parameters` a full OpenAI
|
Removes a connected remote.
|
||||||
tool-parameter schema dict.
|
|
||||||
|
|
||||||
Downstream (backend → the session serving the remote):
|
**Payload**:
|
||||||
|
* `remotename` (string): The name of the remote to remove.
|
||||||
|
|
||||||
~~~json
|
### `remote-list`
|
||||||
{"action": "remote-call", "requestId": "weather-service#0", "function": "get_forecast", "args": {"city": "Rome"}}
|
Requests the list of remotes currently registered for the authenticated user.
|
||||||
~~~
|
|
||||||
|
|
||||||
Result routing: requestIds are `'<remotename>#<n>'`, unique among a user's
|
**Payload**: (Empty)
|
||||||
remotes. A `remote-call-result` is fanned out over the receiving session's
|
|
||||||
own connections; `resolve`/`reject` ignore unknown ids, so one session can
|
|
||||||
serve several remotes without ambiguity. Fan-out is session-scoped, so
|
|
||||||
identical id strings belonging to another user's remote can never be
|
|
||||||
resolved from this session.
|
|
||||||
|
|
||||||
Example connect + call round trip:
|
### `remote-call`
|
||||||
|
Invokes a specific function on a remote.
|
||||||
|
|
||||||
~~~json
|
**Payload**:
|
||||||
{"action": "remote-connect", "nonce": "c1", "body": {"remotename": "weather-service", "tools": [{"name": "get_forecast", "description": "Retrieve weather forecast for a location.", "parameters": {"type": "object", "properties": {"city": {"type": "string", "description": "City name."}}, "required": ["city"]}}]}}
|
* `remotename` (string): The target remote.
|
||||||
~~~
|
* `function` (string): The function name.
|
||||||
|
* `args` (object): The arguments to pass to the function, matching the schema defined in `remote-connect`.
|
||||||
|
|
||||||
~~~json
|
### `remote-call-result`
|
||||||
{"action": "remote-call", "nonce": "c2", "body": {"remotename": "weather-service", "function": "get_forecast", "args": {"city": "Rome"}}}
|
Sent by the server to the active session to deliver the outcome of a pending `remote-call`.
|
||||||
~~~
|
|
||||||
|
|
||||||
~~~json
|
**Payload**:
|
||||||
{"action": "remote-call-result", "requestId": "weather-service#0", "result": {"forecast": "sunny", "city": "Rome"}}
|
* `requestId` (string): A unique identifier for this specific request.
|
||||||
~~~
|
* `result` (any): The return value of the function.
|
||||||
|
* `error` (string): An error message if execution failed.
|
||||||
|
|
||||||
Calls not answered within `REMOTE_CALL_TIMEOUT` (30s, constant in
|
## Agent Integration
|
||||||
`remote.py`) fail; calls pending when the remote disconnects fail
|
|
||||||
immediately. All failures surface as `RemoteError`, which becomes an error
|
|
||||||
tool-result string for the model — never an exception that kills a run, and
|
|
||||||
never a killed caller session.
|
|
||||||
|
|
||||||
## The prompt contract
|
Remotes are integrated into the agent workflow in two stages: receiving context and executing tools.
|
||||||
|
|
||||||
~~~json
|
### Chat Integration (`Agent.received_message`)
|
||||||
{
|
When a user sends a message to a chat thread, the frontend sends a special system resource to mark which remotes should be available for that conversation.
|
||||||
"action": "send-agent-prompt",
|
|
||||||
"body": {
|
|
||||||
"agentId": 7,
|
|
||||||
"instruction": "What's the weather in Rome? Use your tools.",
|
|
||||||
"options": { "remotes": ["weather-service", { "name": "code-repo-indexer", "prefix": "repo_" }] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
~~~
|
|
||||||
|
|
||||||
- `Agent.prompt` already splats options into the run input
|
* In `backend/src/agent.py`, the `received_message` method scans the thread's resources.
|
||||||
(`input={"instruction": ..., "input": ..., **kwargs}`), so `options.remotes`
|
* It looks for items with `subkind: 'remote'`.
|
||||||
lands in `run.input["remotes"]`. No server or app change needed.
|
* It extracts the content of these resources (the remote names) and passes them into the agent run context.
|
||||||
- Each entry is a remote name or `{"name": ..., "prefix": ...}`. Every
|
|
||||||
function of the remote becomes one tool named `prefix + function_name`
|
### Execution Integration (`ExpertAgent.run`)
|
||||||
(empty prefix by default).
|
In `backend/src/agents/expert.py`, the `run` method prepares for execution.
|
||||||
- Collisions: the agent's native tools are listed before remote tools and
|
|
||||||
the expert lookup takes the first match, so native tools win. Pass a
|
* It retrieves the list of remotes from the run input.
|
||||||
`prefix` to disambiguate.
|
* It calls `app.getRemoteTools(user_id, remotes)` to resolve these names into active tool specifications.
|
||||||
- Remotes are resolved once, at run start. A remote not connected at that
|
* These tools are injected into the inference loop. When the AI selects a tool, the expert agent executes it. If a remote fails (timeout or error), the failure is returned as a string error, allowing the AI to retry or handle the failure naturally.
|
||||||
moment is skipped with a console warning and the run proceeds (remotes
|
|
||||||
disconnect independently of prompts). If a remote disconnects mid-run,
|
|
||||||
its pending calls fail and the model sees error tool results.
|
|
||||||
- Existing expert rule unchanged: if the run input already carries its own
|
|
||||||
`tools`, those replace the agent toolset entirely and remotes are not in
|
|
||||||
play.
|
|
||||||
- Users only see their own remotes — the registry key includes `user_id`.
|
|
||||||
@@ -197,5 +197,7 @@
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
|
max-height: 20em;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in new issue
Block a user