mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-05-22 11:15:42 +00:00
Merge 7274e41906 into 7ba1d61e34
This commit is contained in:
commit
8623032256
6 changed files with 188 additions and 0 deletions
8
agent.py
8
agent.py
|
|
@ -240,6 +240,14 @@ class AgentContext:
|
|||
self.task = self.communicate(UserMessage(self.agent0.read_prompt("fw.msg_nudge.md")))
|
||||
return self.task
|
||||
|
||||
@extension.extensible
|
||||
def stop(self):
|
||||
"""Stop the running agent immediately, preserving conversation history."""
|
||||
self.kill_process()
|
||||
self.paused = False
|
||||
self.streaming_agent = None
|
||||
self.log.log(type="info", content="Agent stopped by user.")
|
||||
|
||||
@extension.extensible
|
||||
def get_agent(self):
|
||||
return self.streaming_agent or self.agent0
|
||||
|
|
|
|||
14
api/stop.py
Normal file
14
api/stop.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
from helpers.api import ApiHandler, Request, Response
|
||||
|
||||
|
||||
class Stop(ApiHandler):
|
||||
async def process(self, input: dict, request: Request) -> dict | Response:
|
||||
ctxid = input.get("context", "")
|
||||
if not ctxid:
|
||||
raise Exception("No context id provided")
|
||||
context = self.use_context(ctxid)
|
||||
context.stop()
|
||||
return {
|
||||
"message": "Agent stopped.",
|
||||
"context": context.id,
|
||||
}
|
||||
125
tests/test_stop_agent.py
Normal file
125
tests/test_stop_agent.py
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import sys
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from agent import AgentContext
|
||||
from initialize import initialize_agent
|
||||
from api.stop import Stop
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ctx():
|
||||
ctxid = "ctx-stop-test"
|
||||
context = AgentContext(config=initialize_agent(), id=ctxid, set_current=False)
|
||||
yield context
|
||||
AgentContext.remove(ctxid)
|
||||
|
||||
|
||||
class TestAgentContextStop:
|
||||
"""Unit tests for AgentContext.stop() method."""
|
||||
|
||||
def test_stop_clears_paused_state(self, ctx):
|
||||
ctx.paused = True
|
||||
ctx.stop()
|
||||
assert ctx.paused is False
|
||||
|
||||
def test_stop_clears_streaming_agent(self, ctx):
|
||||
ctx.streaming_agent = MagicMock()
|
||||
ctx.stop()
|
||||
assert ctx.streaming_agent is None
|
||||
|
||||
def test_stop_calls_kill_process(self, ctx):
|
||||
with patch.object(ctx, "kill_process") as mock_kill:
|
||||
ctx.stop()
|
||||
mock_kill.assert_called_once()
|
||||
|
||||
def test_stop_logs_info_message(self, ctx):
|
||||
initial_log_count = len(ctx.log.logs)
|
||||
ctx.stop()
|
||||
assert len(ctx.log.logs) > initial_log_count
|
||||
last_log = ctx.log.logs[-1]
|
||||
assert last_log.type == "info"
|
||||
assert "stopped" in last_log.content.lower()
|
||||
|
||||
def test_stop_preserves_conversation_history(self, ctx):
|
||||
ctx.log.log(type="user", heading="test", content="hello world")
|
||||
log_count_before = len(ctx.log.logs)
|
||||
ctx.stop()
|
||||
# Should have original logs + the "stopped" info log
|
||||
assert len(ctx.log.logs) == log_count_before + 1
|
||||
|
||||
def test_stop_does_not_reset_agent(self, ctx):
|
||||
original_agent = ctx.agent0
|
||||
ctx.stop()
|
||||
assert ctx.agent0 is original_agent
|
||||
|
||||
def test_stop_when_not_running_is_safe(self, ctx):
|
||||
"""Calling stop when nothing is running should not raise."""
|
||||
ctx.task = None
|
||||
ctx.paused = False
|
||||
ctx.streaming_agent = None
|
||||
ctx.stop() # should not raise
|
||||
assert ctx.paused is False
|
||||
|
||||
def test_stop_differs_from_nudge(self, ctx):
|
||||
"""Stop should NOT restart the agent (unlike nudge)."""
|
||||
ctx.stop()
|
||||
# After stop, task should be None (no new task started)
|
||||
# nudge() would set self.task to a new communicate() call
|
||||
assert ctx.task is None
|
||||
|
||||
def test_stop_differs_from_reset(self, ctx):
|
||||
"""Stop should NOT clear the log (unlike reset)."""
|
||||
ctx.log.log(type="user", heading="test", content="preserved")
|
||||
ctx.stop()
|
||||
# reset() calls log.reset() which clears everything
|
||||
assert len(ctx.log.logs) >= 2 # original + stopped message
|
||||
|
||||
|
||||
class TestStopApiHandler:
|
||||
"""Tests for the api/stop.py endpoint."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_api_returns_success(self, ctx):
|
||||
app = Flask("stop-api-test")
|
||||
app.secret_key = "test-secret"
|
||||
lock = threading.RLock()
|
||||
|
||||
handler = Stop(app, lock)
|
||||
result = await handler.process({"context": ctx.id}, None)
|
||||
|
||||
assert result["message"] == "Agent stopped."
|
||||
assert result["context"] == ctx.id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_api_raises_without_context_id(self):
|
||||
app = Flask("stop-api-test")
|
||||
app.secret_key = "test-secret"
|
||||
lock = threading.RLock()
|
||||
|
||||
handler = Stop(app, lock)
|
||||
with pytest.raises(Exception, match="No context id provided"):
|
||||
await handler.process({"context": ""}, None)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_api_actually_stops_agent(self, ctx):
|
||||
ctx.paused = True
|
||||
ctx.streaming_agent = MagicMock()
|
||||
|
||||
app = Flask("stop-api-test")
|
||||
app.secret_key = "test-secret"
|
||||
lock = threading.RLock()
|
||||
|
||||
handler = Stop(app, lock)
|
||||
await handler.process({"context": ctx.id}, None)
|
||||
|
||||
assert ctx.paused is False
|
||||
assert ctx.streaming_agent is None
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
<head>
|
||||
<script type="module">
|
||||
import { store } from "/components/chat/input/input-store.js";
|
||||
import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -22,6 +23,16 @@
|
|||
<span x-text="$store.chatInput.paused ? 'Resume Agent' : 'Pause Agent'"></span>
|
||||
</button>
|
||||
|
||||
<button type="button" class="text-button stop-button"
|
||||
@click="$store.chatInput.stopAgent()"
|
||||
x-show="$store.chats?.selectedContext?.running"
|
||||
title="Stop the agent immediately">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
|
||||
<path d="M6 6h12v12H6z"></path>
|
||||
</svg>
|
||||
<span>Stop Agent</span>
|
||||
</button>
|
||||
|
||||
<button type="button" class="text-button" id="nudges_window" @click="$store.chatInput.nudge()">
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 49 58" fill="currentColor" width="14" height="14" aria-hidden="true">
|
||||
<path d="m11.97,16.32c-.46,0-.91-.25-1.15-.68-.9-1.63-1.36-3.34-1.36-5.1C9.45,4.73,14.18,0,20,0s10.55,4.73,10.55,10.55c0,.87-.13,1.75-.41,2.76-.19.7-.9,1.13-1.62.93-.7-.19-1.12-.92-.93-1.62.21-.79.31-1.44.31-2.07,0-4.36-3.55-7.91-7.91-7.91s-7.91,3.55-7.91,7.91c0,1.3.35,2.59,1.03,3.82.36.64.13,1.44-.51,1.79-.21.11-.42.17-.64.17Z" stroke-width="0.5" stroke="currentColor"/>
|
||||
|
|
@ -77,6 +88,10 @@
|
|||
flex-shrink: 0;
|
||||
}
|
||||
.text-button p { margin-block: 0; }
|
||||
.stop-button { color: var(--color-error, #e74c3c) !important; }
|
||||
.stop-button:hover:not(:disabled) {
|
||||
background-color: rgba(231, 76, 60, 0.1) !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.text-buttons-row {
|
||||
|
|
|
|||
|
|
@ -121,6 +121,18 @@ const model = {
|
|||
}
|
||||
},
|
||||
|
||||
async stopAgent() {
|
||||
try {
|
||||
const context = globalThis.getContext();
|
||||
if (!globalThis.sendJsonData) throw new Error("sendJsonData not available");
|
||||
await globalThis.sendJsonData("/stop", { context });
|
||||
} catch (e) {
|
||||
if (globalThis.toastFetchError) {
|
||||
globalThis.toastFetchError("Error stopping agent", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async loadKnowledge() {
|
||||
try {
|
||||
const resp = await shortcuts.callJsonApi(
|
||||
|
|
|
|||
|
|
@ -524,6 +524,20 @@ globalThis.pauseAgent = async function (paused) {
|
|||
await inputStore.pauseAgent(paused);
|
||||
};
|
||||
|
||||
globalThis.stopAgent = async function () {
|
||||
await inputStore.stopAgent();
|
||||
};
|
||||
|
||||
// Escape key to stop running agent (only when not typing in an input/textarea)
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape" && chatsStore.selectedContext?.running) {
|
||||
const tag = document.activeElement?.tagName;
|
||||
if (tag === "TEXTAREA" || tag === "INPUT") return;
|
||||
e.preventDefault();
|
||||
globalThis.stopAgent();
|
||||
}
|
||||
});
|
||||
|
||||
function generateShortId() {
|
||||
const chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue