Refactor agent execution to use AgentRun with OpenAI-compatible API

Major refactoring to support isolated run objects with streaming, events, and OpenAI-compatible endpoints.

## Backend Changes

- **Agent class**: Added `run()` method, `received_message()` wraps chat into AgentRun
- **AgentRun class**: New class for run state with event system, serialization, and user tracking
- **inference.py**: Complete refactor with `build_messages()`, `chat_completion()`, tool support for llama-cpp/ollama/replicate
- **tools.py**: New centralized tool management (ToolSpec, ToolParameter, format_tools_for_engine)
- **tools.py**: Removed legacy Toolset/Action/ToolDescription system
- **server.py**: Added OpenAI-compatible `/api/openai/v1/chat/completions` endpoint with streaming support
- **temporary.py**: Added `self.runs` dict to State class
- **expert.py**: Refactored to use new Agent.run() signature
- **scripted.py**: Updated to raise NotImplementedError

## Frontend Changes

- **backend.js**: Added `onRunEvents()` for run event subscriptions
- **Sidebar.vue**: Added runs navigation mode
- **router/index.js**: Renamed User route to RunsView
- **RunsView.vue**: New view for listing/monitoring agent runs

## Breaking Changes

- Agent `prompt()` signature changed: `(instruction, input=None, **kwargs)` instead of `(content)`
- Agent `received_message()` must use AgentRun pattern
- Tools.py removed, functionality moved to tool.py
- Deprecated tool prompt/action system removed
This commit is contained in:
Shivering Ocelot committed 2026-05-25 18:05:17 -05:00
1 parent eb9a1ad9e8
commit 4e15b44d39
14 files changed
+1151 -522

No files matched your search

+46 -16
View File
@@ -1,6 +1,7 @@
from datetime import datetime
import asyncio
import json
import time
import sys
import os
@@ -25,6 +26,7 @@ class App(ChatMixin):
self.user_connections = {}
self.remote_connections = {}
self.streams = {}
self.event_listeners = []
self.tmp = tmp.State()
@@ -37,14 +39,25 @@ class App(ChatMixin):
raise Exception("Horde only works with a replicate connection. Please provide an API Token.")
self.agent_classes = [
agent_mod.BasicAgent,
agents_mod.CharacterAgent,
agents_mod.ExpertAgent,
agents_mod.ScriptedAgent,
]
# --- Connection Management (Synchronous) ---
def on_event(self, callback):
"""Register a callback for events."""
if callback not in self.event_listeners:
self.event_listeners.append(callback)
return lambda: self.event_listeners.remove(callback)
def emit_event(self, name, data=None):
"""Emit an event to all listeners."""
for listener in self.event_listeners:
try:
listener(name, data)
except Exception as e:
print(f"Error in event listener: {name}, {data}")
def add_user_connection(self, user_id, channel):
if user_id not in self.user_connections:
self.user_connections[user_id] = []
@@ -62,7 +75,7 @@ class App(ChatMixin):
if user_id not in self.remote_connections:
self.remote_connections[user_id] = {}
user_connections = self.remote_connections[user_id]
if remotename not in user_connections:
user_connections[remotename] = []
if channel not in user_connections[remotename]:
@@ -133,7 +146,7 @@ class App(ChatMixin):
async def update_agent(self, agent_id, name=None, description=None, picture=None, agentname=None, data=None):
agent = await self.get_agent(agent_id)
summary = []
if name is not None:
agent.name = name
@@ -206,7 +219,7 @@ class App(ChatMixin):
)
print(f"Created user #{user.id}: {username} ({email})")
return user.id
else:
else:
print(f"Signup error: '{email}' or '{username}' are already used")
return None
@@ -214,11 +227,11 @@ class App(ChatMixin):
async def get_agent_control(self, agent_id):
agent = await self.get_agent(agent_id)
agent_class = next((kind for kind in self.agent_classes if kind.identifier == agent.kind), None)
if agent_class is None:
raise Exception(f"invalid agent kind: {agent.kind}")
control = agent_class(self, agent.data)
control._set_basic_info(agent_id, agent.name)
return control
@@ -265,7 +278,7 @@ class App(ChatMixin):
kind="conversation",
data={}
)
# --- Resource Management ---
async def get_agent_resource(self, agent_id, kind, key):
@@ -274,8 +287,8 @@ class App(ChatMixin):
async def save_agent_resource(self, agent_id, kind, key, value):
await db.AgentResource.update_or_create(
agent_id=agent_id,
kind=kind,
agent_id=agent_id,
kind=kind,
key=key,
value=value,
)
@@ -291,8 +304,8 @@ class App(ChatMixin):
async def save_model(self, model_id, kind, name, value):
if model_id is None:
model = await db.Model.create(
name=name,
kind=kind,
name=name,
kind=kind,
value=value,
)
else:
@@ -302,14 +315,31 @@ class App(ChatMixin):
return None
else:
model = await db.Model.update_or_create(
id=model_id,
name=name,
kind=kind,
id=model_id,
name=name,
kind=kind,
value=value,
)
print(f"Saved model: {model.id} - {name}")
return model
async def create_agent_run(self, agent_id):
agent = await self.get_agent(agent_id)
run = agent_mod.AgentRun.create(
self,
agent_id=agent_id,
user_id=agent.user_id,
)
return run.id
async def get_agent_runs(self, agent_id=None, user_id=None):
runs = list(self.tmp.runs.values())
if agent_id:
runs = [r for r in runs if r.agent_id == agent_id]
elif user_id:
runs = [r for r in runs if r.user_id == user_id]
return [r.serialize() for r in runs]
# --- Main Entry Point ---
async def main():