mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-19 15:18:03 +00:00
feat(kanban): typed block reasons + unblock-loop breaker (#52848)
* feat(kanban): typed block reasons + unblock-loop breaker Stops the kanban blocked-task loop: a worker blocks a task, a cron unblocks it, the worker re-blocks for the same reason, repeat forever. block_task now takes a typed kind and a persistent block_recurrences counter on the tasks table: - kind=dependency routes to todo (parent-gated, auto-resumed), never the human 'blocked' bucket a cron would keep unblocking. - needs_input/capability/transient/untyped land in blocked; each same-cause re-block after an unblock increments block_recurrences, and at BLOCK_RECURRENCE_LIMIT (default 2) the task routes to triage for a human instead of blocked. - unblock_task no longer resets block_recurrences (the amnesia that let the loop run unbounded); complete_task clears it on success. Wired through the worker kanban_block tool (new kind arg) and the hermes kanban block --kind CLI flag, both reporting where the task actually landed. Docs + 11 new tests; 536 existing kanban tests green. * test(kanban): make second-block notify test use a distinct block cause test_notifier_second_blocked_delivers blocked the same task twice with the same (untyped) reason, which now trips the new unblock-loop breaker and routes the second block to triage instead of blocked — so only one 'blocked' notification fired. The test's actual intent is that TWO distinct block cycles each notify; give the two cycles different kinds (needs_input then capability) so they're genuinely separate blocks. The same-cause loop→triage path is covered by test_kanban_block_kinds.py.
This commit is contained in:
parent
43b8ba4181
commit
5b5c79a8ef
6 changed files with 563 additions and 61 deletions
|
|
@ -623,13 +623,20 @@ def _handle_block(args: dict, **kw) -> str:
|
|||
if not reason or not str(reason).strip():
|
||||
return tool_error("reason is required — explain what input you need")
|
||||
reason = redact_sensitive_text(str(reason), force=True)
|
||||
kind = args.get("kind")
|
||||
board = args.get("board")
|
||||
try:
|
||||
kb, conn = _connect(board=board)
|
||||
if kind is not None and kind not in kb.VALID_BLOCK_KINDS:
|
||||
conn.close()
|
||||
return tool_error(
|
||||
f"kind must be one of {sorted(kb.VALID_BLOCK_KINDS)} (or omit it)"
|
||||
)
|
||||
try:
|
||||
ok = kb.block_task(
|
||||
conn, tid,
|
||||
reason=reason,
|
||||
kind=kind,
|
||||
expected_run_id=_worker_run_id(tid),
|
||||
)
|
||||
if not ok:
|
||||
|
|
@ -638,7 +645,15 @@ def _handle_block(args: dict, **kw) -> str:
|
|||
f"running/ready)"
|
||||
)
|
||||
run = kb.latest_run(conn, tid)
|
||||
return _ok(task_id=tid, run_id=run.id if run else None)
|
||||
# Tell the worker where the task actually landed so it doesn't
|
||||
# assume it's sitting in 'blocked' when routing sent it elsewhere.
|
||||
landed = kb.get_task(conn, tid)
|
||||
return _ok(
|
||||
task_id=tid,
|
||||
run_id=run.id if run else None,
|
||||
status=landed.status if landed else "blocked",
|
||||
block_kind=kind,
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
except ValueError as e:
|
||||
|
|
@ -1192,11 +1207,16 @@ KANBAN_COMPLETE_SCHEMA = {
|
|||
KANBAN_BLOCK_SCHEMA = {
|
||||
"name": "kanban_block",
|
||||
"description": (
|
||||
"Transition the task to blocked because you need human input "
|
||||
"to proceed. ``reason`` will be shown to the human on the "
|
||||
"board and included in context when someone unblocks you. "
|
||||
"Use for genuine blockers only — don't block on things you can "
|
||||
"resolve yourself."
|
||||
"Stop work on this task and route it according to WHY you're stuck. "
|
||||
"Set ``kind`` to say which: 'dependency' (waiting on another task — "
|
||||
"goes to todo and auto-resumes when that task finishes, no human "
|
||||
"needed), 'needs_input' (you need a human decision/answer), "
|
||||
"'capability' (a hard wall: no access, missing credentials, an action "
|
||||
"no agent can do), or 'transient' (a flaky failure that may clear). "
|
||||
"``reason`` is shown to the human on the board. If a task keeps "
|
||||
"getting unblocked and re-blocked for the same reason, it is "
|
||||
"auto-escalated to triage. Use for genuine blockers only — don't "
|
||||
"block on things you can resolve yourself."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
|
|
@ -1208,9 +1228,18 @@ KANBAN_BLOCK_SCHEMA = {
|
|||
"reason": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"What you need answered, in one or two sentences. "
|
||||
"Don't paste the whole conversation; the human has "
|
||||
"the board and can ask follow-ups via comments."
|
||||
"What you need answered or what stopped you, in one or "
|
||||
"two sentences. Don't paste the whole conversation; the "
|
||||
"human has the board and can ask follow-ups via comments."
|
||||
),
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": ["dependency", "needs_input", "capability", "transient"],
|
||||
"description": (
|
||||
"Why you're blocked. 'dependency' waits in todo and "
|
||||
"resumes automatically; the others surface to a human. "
|
||||
"Omit only if none apply."
|
||||
),
|
||||
},
|
||||
"board": _board_schema_prop(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue