mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-08-17 12:25:14 +00:00
Stop idle goal polling
Publish goal revisions through the shared state monitor and refresh the Goal UI only when the selected chat or goal state changes. Keep the local elapsed-time clock, remove recurring API requests, and cover the state-push contract with focused regressions.
This commit is contained in:
parent
4f8ae9cc2c
commit
e8e566d221
6 changed files with 95 additions and 13 deletions
|
|
@ -27,6 +27,9 @@
|
|||
- `/goal auto` fills the composer with a prompt asking the agent to create and manage its own goal instead of silently sending a message.
|
||||
- While a goal is active, response-tool calls are intermediate updates; only completing or blocking the goal restores normal loop termination.
|
||||
- Goal UI feedback uses toast notifications and inline controls, not modal dialogs.
|
||||
- Goal state changes publish a context revision through the shared state-push
|
||||
lifecycle; the WebUI refreshes on context or revision changes and never polls
|
||||
the Goal API while idle.
|
||||
|
||||
## Work Guidance
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
import { store as goalStore } from "/plugins/_goal/webui/goal-store.js";
|
||||
|
||||
let lastContextId = "";
|
||||
let lastRevision = null;
|
||||
|
||||
export default async function refreshGoalOnRevision(ctx) {
|
||||
const snapshot = ctx?.snapshot;
|
||||
const contextId = String(snapshot?.context || "");
|
||||
const activeContext = (snapshot?.contexts || []).find(item => item?.id === contextId);
|
||||
const revision = activeContext?._goal_revision ?? null;
|
||||
|
||||
if (contextId === lastContextId && revision === lastRevision) return;
|
||||
lastContextId = contextId;
|
||||
lastRevision = revision;
|
||||
await goalStore.refresh(true);
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@
|
|||
<div x-data
|
||||
class="goal-strip-root"
|
||||
x-create="$store.goalBar.onMount()"
|
||||
x-init="$watch('$store.chats.selected', () => $store.goalBar.refresh(true))"
|
||||
x-destroy="$store.goalBar.cleanup()">
|
||||
<template x-if="$store.goalBar && $store.goalBar.visible">
|
||||
<div class="goal-strip" :class="`is-${$store.goalBar.goal?.status || 'active'}`">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
|
@ -57,6 +58,56 @@ def test_goal_storage_round_trip(context_id: str):
|
|||
assert goal.get_goal(context_id) is None
|
||||
|
||||
|
||||
def test_goal_changes_publish_state_revision(context_id: str, monkeypatch):
|
||||
from agent import AgentContext
|
||||
from helpers import state_monitor_integration
|
||||
|
||||
revisions = iter([1.0, 2.0, 3.0])
|
||||
output_data = {}
|
||||
dirty = []
|
||||
context = SimpleNamespace(
|
||||
set_output_data=lambda key, value: output_data.__setitem__(key, value)
|
||||
)
|
||||
monkeypatch.setattr(AgentContext, "get", lambda _context_id: context)
|
||||
monkeypatch.setattr(goal.time, "time", lambda: next(revisions))
|
||||
monkeypatch.setattr(
|
||||
state_monitor_integration,
|
||||
"mark_dirty_for_context",
|
||||
lambda context_id, *, reason: dirty.append((context_id, reason)),
|
||||
)
|
||||
|
||||
goal.create_goal(context_id, "Publish changes")
|
||||
goal.update_goal(context_id, status="paused")
|
||||
goal.delete_goal(context_id)
|
||||
|
||||
assert output_data["_goal_revision"] == 3.0
|
||||
assert dirty == [(context_id, "plugins._goal")] * 3
|
||||
|
||||
|
||||
def test_goal_webui_uses_state_revisions_instead_of_polling():
|
||||
plugin_root = Path(__file__).resolve().parents[1]
|
||||
store = (plugin_root / "webui" / "goal-store.js").read_text()
|
||||
strip = (
|
||||
plugin_root
|
||||
/ "extensions"
|
||||
/ "webui"
|
||||
/ "chat-input-progress-start"
|
||||
/ "goal-strip.html"
|
||||
).read_text()
|
||||
refresh = (
|
||||
plugin_root
|
||||
/ "extensions"
|
||||
/ "webui"
|
||||
/ "apply_snapshot_before"
|
||||
/ "refresh-goal.js"
|
||||
).read_text()
|
||||
|
||||
assert "setInterval(() => this.refresh" not in store
|
||||
assert "$watch('$store.chats.selected'" not in strip
|
||||
assert "_goal_revision" in refresh
|
||||
assert "goalStore.refresh(true)" in refresh
|
||||
|
||||
|
||||
def test_goal_command_sets_pauses_resumes_and_deletes(context_id: str):
|
||||
created = goal_command.run(_payload(context_id, "/goal Add current goal support"))
|
||||
assert created["effects"][0]["message"] == "Goal set."
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
|||
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
|
@ -141,7 +142,9 @@ def update_goal(
|
|||
|
||||
|
||||
def delete_goal(context_id: str) -> None:
|
||||
files.delete_file(_goal_path(_require_context_id(context_id)))
|
||||
context_id = _require_context_id(context_id)
|
||||
files.delete_file(_goal_path(context_id))
|
||||
_notify_goal_changed(context_id)
|
||||
|
||||
|
||||
def public_goal(goal: dict[str, Any] | None) -> dict[str, Any] | None:
|
||||
|
|
@ -182,6 +185,23 @@ def _write_goal(goal: dict[str, Any]) -> None:
|
|||
_goal_path(str(goal["context_id"])),
|
||||
json.dumps(public_goal(goal), indent=2, ensure_ascii=False) + "\n",
|
||||
)
|
||||
_notify_goal_changed(str(goal["context_id"]))
|
||||
|
||||
|
||||
def _notify_goal_changed(context_id: str) -> None:
|
||||
from agent import AgentContext
|
||||
|
||||
context = AgentContext.get(context_id)
|
||||
if context is None:
|
||||
return
|
||||
context.set_output_data("_goal_revision", time.time())
|
||||
|
||||
try:
|
||||
from helpers.state_monitor_integration import mark_dirty_for_context
|
||||
|
||||
mark_dirty_for_context(context_id, reason="plugins._goal")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _normalize_goal(raw: dict[str, Any], *, context_id: str) -> dict[str, Any]:
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ const model = {
|
|||
editing: false,
|
||||
draft: "",
|
||||
lastContextId: "",
|
||||
intervalId: null,
|
||||
clockIntervalId: null,
|
||||
goalChangedHandler: null,
|
||||
now: Date.now(),
|
||||
|
|
@ -82,17 +81,15 @@ const model = {
|
|||
document.getElementById("progress-bar-box")?.classList.add("has-goal-bar");
|
||||
this.goalChangedHandler = (event) => {
|
||||
const detail = event?.detail || {};
|
||||
if (detail.goal === null) {
|
||||
this.goal = null;
|
||||
}
|
||||
void this.refresh(true);
|
||||
if (detail.context_id && detail.context_id !== this.contextId) return;
|
||||
this.goal = detail.goal || null;
|
||||
this.now = Date.now();
|
||||
if (!this.goal) this.editing = false;
|
||||
};
|
||||
window.addEventListener("goal:changed", this.goalChangedHandler);
|
||||
this.clockIntervalId = window.setInterval(() => {
|
||||
this.now = Date.now();
|
||||
}, 1000);
|
||||
this.intervalId = window.setInterval(() => this.refresh(), 3000);
|
||||
void this.refresh(true);
|
||||
},
|
||||
|
||||
cleanup() {
|
||||
|
|
@ -100,14 +97,10 @@ const model = {
|
|||
if (this.goalChangedHandler) {
|
||||
window.removeEventListener("goal:changed", this.goalChangedHandler);
|
||||
}
|
||||
if (this.intervalId) {
|
||||
window.clearInterval(this.intervalId);
|
||||
}
|
||||
if (this.clockIntervalId) {
|
||||
window.clearInterval(this.clockIntervalId);
|
||||
}
|
||||
this.goalChangedHandler = null;
|
||||
this.intervalId = null;
|
||||
this.clockIntervalId = null;
|
||||
},
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue