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:
1 parent
eb9a1ad9e8
commit
4e15b44d39
14 files changed
+1151
-522
No files matched your search
@@ -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", {});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')),
|
||||
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user