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

+26
View File
@@ -30,6 +30,7 @@ let credentialsDeferred = createDeferred();
const pendingActions = {}; // Maps nonce to { resolve, reject }
const chatListeners = {}; // Maps chatId to an array of callback functions
const orphanHandlers = []; // Queue of callbacks for orphan server messages
const runListeners = []; // Queue of callbacks for run events
function createDeferred() {
let resolve, reject;
@@ -191,6 +192,15 @@ function handleIncomingMessage(rawData) {
} else {
console.log(`No active listeners for chatId ${chatId}`);
}
} else if (packet.action == 'run-event') {
// Broadcast run events to all listeners
runListeners.forEach(callback => {
try {
callback(packet);
} catch (e) {
console.error(`Error in run event listener:`, e);
}
});
} else {
if (orphanHandlers.length) {
const f = orphanHandlers.shift();
@@ -247,6 +257,22 @@ export function onChatMessages(chatId, callback) {
};
}
export function onRunEvents(callback) {
if (typeof callback !== 'function') {
throw new TypeError("onRunEvents: callback must be a function.");
}
runListeners.push(callback);
// Return an unsubscribe function
return () => {
const index = runListeners.indexOf(callback);
if (index > -1) {
runListeners.splice(index, 1);
}
};
}
export async function getChats() {
if (cache.chats === null) {
const response = await sendAction("get-chats", {});
+4 -1
View File
@@ -101,6 +101,8 @@
</button>
</div>
</div>
<nav v-if="mode=='runs'">
</nav>
<nav v-if="mode=='chats'">
<RouterLink v-for="it in chats" :to="'/chat/' + it.id" class="nav-item">{{it.name}}</RouterLink>
<RouterLink to="/chat/create" class="nav-item">Create Chat</RouterLink>
@@ -110,8 +112,9 @@
<RouterLink to="/agent/create" class="nav-item">Create Agent</RouterLink>
</nav>
<nav v-if="mode=='settings'">
<RouterLink to="/models" class="nav-item">Manage Models</RouterLink>
<RouterLink to="/user" class="nav-item">User</RouterLink>
<RouterLink to="/models" class="nav-item">Manage Models</RouterLink>
<RouterLink to="/runs" class="nav-item">Agent Runs</RouterLink>
</nav>
</aside>
</template>
+3 -3
View File
@@ -44,9 +44,9 @@ const router = createRouter({
component: () => import('../views/Models.vue')
},
{
name: 'user',
path: `/user`,
component: () => import('../views/User.vue')
name: 'runs',
path: `/runs`,
component: () => import('../views/RunsView.vue')
},
objectView('chat', () => import('../views/Chat.vue')),
objectView('agent', () => import('../views/Agent.vue')),
+218
View File
@@ -0,0 +1,218 @@
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import TitleBar from '../components/TitleBar.vue';
import * as backend from '../backend';
const runs = ref([]);
const expandedRunId = ref(null);
const loading = ref(true);
let unsubscribeRunEvents = null;
const toggleRun = (id) => {
if (expandedRunId.value === id) {
expandedRunId.value = null;
} else {
expandedRunId.value = id;
}
};
const formatTime = (timestamp) => {
if (!timestamp) return '—';
return new Date(timestamp * 1000).toLocaleString();
};
const getStatusBadge = (run) => {
return run.done ? 'Done' : 'Running';
};
const updateRun = (event) => {
const { name, data } = event;
// Find the run by ID
const runIndex = runs.value.findIndex(r => r.id === data.runId);
if (runIndex === -1) {
// New run was created
if (name === 'run-started') {
runs.value.push(data);
}
} else {
// Update existing run
const run = runs.value[runIndex];
if (name === 'run-completed') {
run.done = true;
run.endTime = data.endTime;
run.output = data.output;
// Mark as expanded to show the completion
if (expandedRunId.value !== data.runId) {
expandedRunId.value = data.runId;
}
} else if (name === 'run-event') {
// Add new event to the run
if (!run.events) {
run.events = [];
}
run.events.push(data.data);
// If this run is expanded, update it
if (expandedRunId.value === data.runId) {
// No DOM update needed as we're re-rendering the whole list
}
}
}
};
onMounted(async () => {
try {
const res = await backend.sendAction("get-agent-runs");
runs.value = res.runs || [];
} catch (err) {
console.error("Failed to load agent runs:", err);
} finally {
loading.value = false;
}
// Listen for live run events
unsubscribeRunEvents = backend.onRunEvents(updateRun);
});
onUnmounted(() => {
if (unsubscribeRunEvents) {
unsubscribeRunEvents();
}
});
</script>
<template>
<main>
<TitleBar>
<h1>Agent Runs</h1>
</TitleBar>
<div v-if="loading" class="loading">Loading runs...</div>
<div v-else-if="runs.length === 0" class="empty">
No agent runs found.
</div>
<div v-else v-for="run in runs" :key="run.id" class="run-entry">
<div class="run-header" @click="toggleRun(run.id)">
<span class="run-id">{{ run.id }}</span>
<span class="run-agent">Agent: {{ run.agentId }}</span>
<span :class="['run-status', run.done ? 'status-done' : 'status-running']">
{{ getStatusBadge(run) }}
</span>
</div>
<div v-if="expandedRunId === run.id" class="run-details">
<div class="detail-row">
<div class="detail-key">User ID</div>
<div class="detail-value">{{ run.userId }}</div>
</div>
<div class="detail-row">
<div class="detail-key">Start Time</div>
<div class="detail-value">{{ formatTime(run.startTime) }}</div>
</div>
<div class="detail-row">
<div class="detail-key">End Time</div>
<div class="detail-value">{{ formatTime(run.endTime) }}</div>
</div>
<div class="detail-row">
<div class="detail-key">Input</div>
<pre class="detail-pre">{{ JSON.stringify(run.input, null, 2) }}</pre>
</div>
<div v-if="run.events && run.events.length > 0" class="detail-row">
<div class="detail-key">Events ({{ run.events.length }})</div>
<pre class="detail-pre">{{ JSON.stringify(run.events, null, 2) }}</pre>
</div>
<div v-if="run.output !== null && run.output !== undefined" class="detail-row">
<div class="detail-key">Output</div>
<pre class="detail-pre">{{ JSON.stringify(run.output, null, 2) }}</pre>
</div>
</div>
</div>
</main>
</template>
<style scoped>
.loading,
.empty {
padding: 1em;
color: var(--color-text-mute);
}
.run-entry {
border-bottom: 1px solid var(--color-border);
}
.run-header {
padding: 0.5em;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
gap: 1em;
}
.run-header:hover {
background-color: var(--color-background-hover);
}
.run-id {
font-family: monospace;
font-size: 0.85em;
color: var(--color-text-heading);
}
.run-agent {
color: var(--color-text-mute);
font-size: 0.85em;
}
.run-status {
font-size: 0.8em;
padding: 0.15em 0.5em;
border-radius: 4px;
font-weight: bold;
}
.status-done {
background-color: var(--color-background-success, #166534);
color: #bbf7d0;
}
.status-running {
background-color: var(--color-background-warn, #854d0e);
color: #fef08a;
}
.run-details {
padding: 0.5em 1em;
background-color: var(--color-background-mute);
}
.detail-row {
margin-bottom: 0.5em;
}
.detail-key {
font-weight: bold;
color: var(--color-text-heading);
margin-bottom: 0.15em;
}
.detail-value {
color: var(--color-text);
}
.detail-pre {
margin: 0;
padding: 0.5em;
background-color: var(--color-background-muted, #111);
border-radius: 4px;
white-space: pre-wrap;
word-break: break-all;
font-size: 0.85em;
}
</style>