mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-20 15:33:54 +00:00
fix(api): stop producers after run transport expires
This commit is contained in:
parent
8f18fa104f
commit
837077dfae
4 changed files with 30 additions and 9 deletions
|
|
@ -4304,6 +4304,11 @@ class APIServerAdapter(BasePlatformAdapter):
|
|||
|
||||
event_cb = self._make_run_event_callback(run_id, loop)
|
||||
|
||||
def _put_event_if_active(event: Optional[Dict]) -> None:
|
||||
"""Enqueue only while this run still owns live transport state."""
|
||||
if self._run_streams.get(run_id) is q:
|
||||
q.put_nowait(event)
|
||||
|
||||
# Also wire stream_delta_callback so message.delta events flow through.
|
||||
def _text_cb(delta: Optional[str]) -> None:
|
||||
if delta is None:
|
||||
|
|
@ -4311,7 +4316,7 @@ class APIServerAdapter(BasePlatformAdapter):
|
|||
if run_id not in self._run_streams:
|
||||
return
|
||||
try:
|
||||
loop.call_soon_threadsafe(q.put_nowait, {
|
||||
loop.call_soon_threadsafe(_put_event_if_active, {
|
||||
"event": "message.delta",
|
||||
"run_id": run_id,
|
||||
"timestamp": time.time(),
|
||||
|
|
@ -4335,7 +4340,7 @@ class APIServerAdapter(BasePlatformAdapter):
|
|||
try:
|
||||
self._set_run_status(run_id, "running")
|
||||
if run_id in self._stopping_run_ids:
|
||||
q.put_nowait({
|
||||
_put_event_if_active({
|
||||
"event": "run.cancelled",
|
||||
"run_id": run_id,
|
||||
"timestamp": time.time(),
|
||||
|
|
@ -4431,7 +4436,7 @@ class APIServerAdapter(BasePlatformAdapter):
|
|||
|
||||
result, usage = await asyncio.get_running_loop().run_in_executor(None, _run_sync)
|
||||
if run_id in self._stopping_run_ids:
|
||||
q.put_nowait({
|
||||
_put_event_if_active({
|
||||
"event": "run.cancelled",
|
||||
"run_id": run_id,
|
||||
"timestamp": time.time(),
|
||||
|
|
@ -4446,7 +4451,7 @@ class APIServerAdapter(BasePlatformAdapter):
|
|||
# block below never fires — issue #15561).
|
||||
elif isinstance(result, dict) and result.get("failed"):
|
||||
error_msg = _redact_api_error_text(result.get("error") or "agent run failed")
|
||||
q.put_nowait({
|
||||
_put_event_if_active({
|
||||
"event": "run.failed",
|
||||
"run_id": run_id,
|
||||
"timestamp": time.time(),
|
||||
|
|
@ -4460,7 +4465,7 @@ class APIServerAdapter(BasePlatformAdapter):
|
|||
)
|
||||
else:
|
||||
final_response = result.get("final_response", "") if isinstance(result, dict) else ""
|
||||
q.put_nowait({
|
||||
_put_event_if_active({
|
||||
"event": "run.completed",
|
||||
"run_id": run_id,
|
||||
"timestamp": time.time(),
|
||||
|
|
@ -4481,7 +4486,7 @@ class APIServerAdapter(BasePlatformAdapter):
|
|||
last_event="run.cancelled",
|
||||
)
|
||||
try:
|
||||
q.put_nowait({
|
||||
_put_event_if_active({
|
||||
"event": "run.cancelled",
|
||||
"run_id": run_id,
|
||||
"timestamp": time.time(),
|
||||
|
|
@ -4498,7 +4503,7 @@ class APIServerAdapter(BasePlatformAdapter):
|
|||
last_event="run.failed",
|
||||
)
|
||||
try:
|
||||
q.put_nowait({
|
||||
_put_event_if_active({
|
||||
"event": "run.failed",
|
||||
"run_id": run_id,
|
||||
"timestamp": time.time(),
|
||||
|
|
@ -4520,7 +4525,7 @@ class APIServerAdapter(BasePlatformAdapter):
|
|||
pass
|
||||
# Sentinel: signal SSE stream to close
|
||||
try:
|
||||
q.put_nowait(None)
|
||||
_put_event_if_active(None)
|
||||
except Exception:
|
||||
pass
|
||||
self._active_run_agents.pop(run_id, None)
|
||||
|
|
|
|||
|
|
@ -540,7 +540,11 @@ class TestRunLifecycleSweep:
|
|||
adapter._sweep_orphaned_runs_once(time.time())
|
||||
before = expired_queue.qsize()
|
||||
stream_delta("must-not-buffer")
|
||||
await asyncio.sleep(0)
|
||||
mock_agent.interrupt("finish test")
|
||||
for _ in range(40):
|
||||
if run_id not in adapter._active_run_tasks:
|
||||
break
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
assert expired_queue.qsize() == before
|
||||
|
||||
|
|
|
|||
|
|
@ -276,9 +276,18 @@ Statuses are retained briefly after terminal states (`completed`, `failed`, or `
|
|||
|
||||
Server-Sent Events stream of the run's tool-call progress, token deltas, and lifecycle events. Designed for dashboards and thick clients that want to attach/detach without losing state.
|
||||
|
||||
Unconsumed event buffers expire after five minutes so a detached client cannot
|
||||
grow memory indefinitely. This expires transport state only: a run that is
|
||||
still executing remains visible to status polling, approval, stop control, and
|
||||
concurrency accounting until its executor work actually exits. A connected SSE
|
||||
subscriber continues draining normally.
|
||||
|
||||
### POST /v1/runs/\{run_id\}/stop
|
||||
|
||||
Interrupt a running agent turn. The endpoint returns immediately with `{"status": "stopping"}` while Hermes asks the active agent to stop at the next safe interruption point.
|
||||
The run stays tracked as `stopping` until the executor-backed work exits, then
|
||||
settles as `cancelled`; requesting stop never hides a worker that is still
|
||||
running.
|
||||
|
||||
### POST /v1/runs/\{run_id\}/approval
|
||||
|
||||
|
|
|
|||
|
|
@ -270,9 +270,12 @@ Runs 接受简单的 `input` 字符串,以及可选的 `session_id`、`instruc
|
|||
|
||||
run 的工具调用进度、token 增量和生命周期事件的 Server-Sent Events 流。专为需要附加/分离而不丢失状态的仪表板和厚客户端设计。
|
||||
|
||||
未消费的事件缓冲区会在五分钟后过期,避免已断开的客户端导致内存无限增长。这里只会过期传输状态:仍在执行的 run 会继续保留在状态轮询、审批、停止控制和并发计数中,直到其 executor 工作真正退出。已连接的 SSE 订阅者会继续正常消费事件。
|
||||
|
||||
### POST /v1/runs/\{run_id\}/stop
|
||||
|
||||
中断正在运行的 agent 轮次。端点立即返回 `{"status": "stopping"}`,同时 Hermes 要求活跃 agent 在下一个安全中断点停止。
|
||||
run 会保持 `stopping` 并继续被跟踪,直到 executor 支持的工作退出,然后进入 `cancelled`;停止请求不会隐藏仍在运行的 worker。
|
||||
|
||||
## Jobs API(后台计划任务)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue