mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Pre-fix, agents had no way to introspect what verbs were valid from a
task's current state — the 2026-05-08 trace showed them spamming
escalate_to_ceo/complete/unblock/resume against a `claimed` task and
racking up rejections. Pre-gateway agents could ground reasoning in
VALID_TRANSITIONS[status] from a doc; the gateway hid that.
Now every Envelope carries:
- current_state: the task's status string (or None for tool-discovery
envelopes that aren't task-bound)
- valid_next_verbs: the verbs the caller can usefully call right now,
sourced from verb_gates.valid_next_verbs(role, task)
Wired into i_will_work_on (highest-traffic verb) for both the OK path
and the wrong-state rejection path. Remaining lifecycle verbs to be
wired in subsequent commits (Task 3.9).
61 lines
2.4 KiB
Python
61 lines
2.4 KiB
Python
"""Envelope must carry current_state + valid_next_verbs after Task 3."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
|
|
from roboco.services.gateway.envelope import Envelope
|
|
|
|
|
|
def test_envelope_ok_carries_introspection_when_task_supplied() -> None:
|
|
task = SimpleNamespace(id="abc", status="in_progress", task_type="code")
|
|
env = Envelope.ok(
|
|
status="in_progress",
|
|
task_id="abc",
|
|
next="commit(message='...')",
|
|
).with_introspection(task=task, role="developer")
|
|
body = env.as_dict()
|
|
assert body["current_state"] == "in_progress"
|
|
assert "commit" in body["valid_next_verbs"]
|
|
assert "i_am_done" in body["valid_next_verbs"]
|
|
|
|
|
|
def test_envelope_error_carries_introspection_too() -> None:
|
|
task = SimpleNamespace(id="abc", status="claimed", task_type="code")
|
|
env = Envelope.invalid_state(
|
|
message="task is in claimed, expected awaiting_pm_review",
|
|
remediate="wait for QA + docs to complete",
|
|
).with_introspection(task=task, role="main_pm")
|
|
body = env.as_dict()
|
|
assert body["error"] == "invalid_state"
|
|
assert body["current_state"] == "claimed"
|
|
# main_pm on a claimed task DOES see `delegate` (legal in claimed),
|
|
# but the lifecycle-spam pattern verbs we want to suppress on
|
|
# `pending` are absent there. Pin the shape rather than the contents.
|
|
assert isinstance(body["valid_next_verbs"], list)
|
|
|
|
|
|
def test_envelope_without_introspection_omits_fields() -> None:
|
|
"""Backwards-compat: callers that don't supply a task get None."""
|
|
env = Envelope.ok(status="ok", task_id=None, next="continue")
|
|
body = env.as_dict()
|
|
assert body["current_state"] is None
|
|
assert body["valid_next_verbs"] is None
|
|
|
|
|
|
def test_with_introspection_returns_self_for_chaining() -> None:
|
|
task = SimpleNamespace(id="x", status="pending", task_type="code")
|
|
env = Envelope.ok(status="pending", task_id="x", next="i_will_work_on")
|
|
result = env.with_introspection(task=task, role="developer")
|
|
assert result is env
|
|
|
|
|
|
def test_with_introspection_handles_unknown_role() -> None:
|
|
"""Unknown role -> empty list, not None."""
|
|
task = SimpleNamespace(id="x", status="pending", task_type="code")
|
|
env = Envelope.ok(status="pending", task_id="x", next="???")
|
|
env.with_introspection(task=task, role="space_marine")
|
|
body = env.as_dict()
|
|
assert body["valid_next_verbs"] == []
|
|
assert body["current_state"] == "pending"
|