Commit graph

2 commits

Author SHA1 Message Date
Ouroboros
acba372ca4 delegate_message: discovery inside the deadline, failed-read note, gateway closed on handshake failure
Both capability reads are bounded by the call's remaining budget and the
deadline is checked between them (a spent budget is delivery_unknown /
deadline_exhausted with nothing sent, never a thread-kill mid-wire); a
FAILED capability read gets its own note (channel unknown, not absent: keep
the run, re-check later) instead of the cancel-and-restart guidance meant
for a missing channel; a handshake failure closes the gateway; the notes
and docs name reconciliation as the timeline rows carrying this messageId
(message.* receipts and the harness status row) rather than message.* only.

Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
2026-09-26 09:31:03 +03:00
Ouroboros
c5afa387db feat(delegate): delegate_message places a live message into a running run
The fifth nanny verb, delegate_message(run_id, text, message_id=""), lives
beside _delegate_answer in delegate_interactions.py: custody-gated through
_owned_run, one internal 100 s deadline under its 120 s ToolEntry timeout,
typed end to end. A FRESH message never POSTs to a settled run (not_active,
reason run_settled) or to an incapable route (unsupported): the engine's
/v2/operations catalog must list the operation AND the route's
agent-capabilities row must declare a liveInput other than none; an
unreadable read is unsupported too, never a guess. Outcomes mirror the
engine's LiveMessageOutcome 1:1 with the reason relayed verbatim, plus the
host's own not_found for ANY 404 after both positive reads (custody
untouched; daemon_says_absent is never consulted). The typed problem code
is read first: 409 idempotency_conflict is rejected (an agent fault), the
other 409s, every 5xx, transport death and deadline exhaustion are
delivery_unknown, 400/413/422 bodies a payload rejected.

The host mints message_id (uuid4, the wire Idempotency-Key) and returns it
in every result; a call carrying a returned id skips both short-circuits
and POSTs so the engine replays the stored receipt, which is the recovery
for delivery_unknown and only for it. No retry loop, no stall detector, no
custody row, no harness-name branch. The receipt is the
delegate_message_outcome event (digest and size, never the text).

Registration: ToolEntry after delegate_answer, docstrings say five verbs,
_AGENT_FAULT_REASONS gains message_text_required and idempotency_conflict,
both child tool profiles, nanny_pacing DELEGATE_ACTIVITY_TOOLS (not a
baseline reset), the cybergym disabled lists, and the delegate_answer /
delegate_wait descriptions become capability-based (delegate_wait names the
timeline's message.* rows as the reconciler; delegate_progress keeps their
messageId and outcome). The exact-set pins and the family refusal table are
updated; the fake daemon serves /v2/operations, liveInput on its harness
row and the messages route with Idempotency-Key replay and typed outcomes
at HTTP 200. tests/test_delegate_message.py pins the gateway, every typed
outcome, the message_id custody, the registration surfaces and the fake
daemon contract against the real client.

ouroboros/safety.py is a protected path and is deliberately NOT edited: the
coordinator adds "delegate_message": POLICY_SKIP after line 127, which
tests/test_safety_policy.py::test_tool_policy_covers_all_builtin_tools
requires (the one expected red until then).

Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
2026-09-26 08:10:21 +03:00