Skip to content

fix(runtime): gate phantom-action guard on text-reply-is-delivery#1228

Open
benhoverter wants to merge 1 commit into
RightNow-AI:mainfrom
benhoverter:fix/phantom-action-channel-reply-aware
Open

fix(runtime): gate phantom-action guard on text-reply-is-delivery#1228
benhoverter wants to merge 1 commit into
RightNow-AI:mainfrom
benhoverter:fix/phantom-action-channel-reply-aware

Conversation

@benhoverter

Copy link
Copy Markdown
Contributor

Summary

The phantom-action guard in run_agent_loop re-prompts the model when it emits
action-shaped text (e.g. "I sent the message to the channel.") without calling
a tool. In channel-reply paths (Discord, Telegram, …) the bridge delivers the
agent's text response verbatim back to the originating channel — the text is
the delivery
, not a hallucinated tool call. The guard misfires on these
legitimate turns: it either re-prompts the model or injects a "claimed action
but did not call any tools"
system reminder that the next turn then argues with.

Fix

Thread a text_reply_is_delivery: bool from each kernel entry point through
execute_llm_agent into run_agent_loop, and gate the phantom detector on
!text_reply_is_delivery.

  • The detector logic is unchanged — only the gating condition becomes more
    conservative (never more aggressive). Non-channel callers see identical behavior.
  • Channel adapters (KernelBridgeAdapter) opt in via new wrappers
    send_message_channel_reply{,_with_blocks} (pass true).
  • Cron, peer-to-peer agent_send, and API-direct paths keep the
    default false — detector behaves exactly as before for them.
  • The streaming variant (run_agent_loop_streaming) does not reference the
    phantom detector, so it is untouched.

Plumbing

  • agent_loop::run_agent_loop — new trailing param text_reply_is_delivery
  • kernel::send_message_with_handle_and_blocks — new trailing param
  • kernel::execute_llm_agent — new trailing param, forwarded
  • kernel::send_message_channel_reply{,_with_blocks} — new wrappers (pass true)
  • KernelBridgeAdapter::send_message{,_with_blocks} — switched to the channel-reply wrappers
  • kernel::send_message{,_with_blocks,_with_handle} — pass false
  • routes::send_message (API direct) — passes false
  • Cron delivery (send_message_with_handle) — passes false

Test plan

  • phantom_guard_fires_when_text_reply_is_delivery_false — confirms unchanged
    behavior for non-channel callers.
  • phantom_guard_suppressed_when_text_reply_is_delivery_true — confirms the bug
    is fixed for channel-reply callers.
  • Both use a PhantomShapedDriver returning action-shaped text on iteration 0 and
    a distinct marker on iteration 1, asserting on which turn's output is delivered.

Verified locally:

  • cargo test -p openfang-runtime --lib → 995/995 pass
  • cargo test -p openfang-kernel --lib → 289/289 pass
  • cargo test -p openfang-api --lib → 92/92 pass
  • cargo clippy -p openfang-{runtime,kernel,api} --all-targets -- -D warnings → clean

The phantom-action guard in `run_agent_loop` re-prompts the LLM when it
emits action-shaped text (e.g. "I sent the message to the channel.")
without calling any tool. In channel-reply paths (Discord, Telegram,
etc.), however, the bridge delivers the agent's text response verbatim
back to the originating channel — the text IS the delivery, not a
hallucinated tool call. The guard misfires on these legitimate turns
and either re-prompts the model or surfaces a "claimed action but did
not call any tools" system reminder that the next turn tries to argue
with.

Fix: thread a `text_reply_is_delivery: bool` from each kernel entry
point through `execute_llm_agent` into `run_agent_loop`, and gate the
phantom detector on `!text_reply_is_delivery`. Channel adapters
(`KernelBridgeAdapter`) opt in via new wrappers
`send_message_channel_reply{,_with_blocks}`; cron, peer-to-peer
agent_send, and API direct paths keep the default `false` and the
detector behaves unchanged for them.

The detector logic itself is unchanged — only the gating condition
becomes more conservative (never more aggressive). The streaming
variant (`run_agent_loop_streaming`) does not reference the phantom
detector, so no change there.

Plumbing:
  * `agent_loop::run_agent_loop`: new last param `text_reply_is_delivery`
  * `kernel::send_message_with_handle_and_blocks`: new last param
  * `kernel::execute_llm_agent`: new last param, forwarded
  * `kernel::send_message_channel_reply{,_with_blocks}`: new wrappers
    that pass `true`
  * `KernelBridgeAdapter::send_message{,_with_blocks}`: switched to
    the new channel-reply wrappers
  * `kernel::send_message{,_with_blocks,_with_handle}`: pass `false`
  * `routes::send_message` API direct call: passes `false`
  * Cron path (`send_message_with_handle` at delivery): passes `false`

Tests:
  * `phantom_guard_fires_when_text_reply_is_delivery_false` — confirms
    unchanged behavior for non-channel callers.
  * `phantom_guard_suppressed_when_text_reply_is_delivery_true` —
    confirms the bug is fixed for channel-reply callers.

Both use a `PhantomShapedDriver` that returns action-shaped text on
iteration 0 and a distinct marker on iteration 1, so the test asserts
on which turn's output is delivered.

Verified:
  * `cargo test -p openfang-runtime --lib`  → 995/995 pass
  * `cargo test -p openfang-kernel --lib`   → 289/289 pass
  * `cargo test -p openfang-api --lib`      →  92/92 pass
  * `cargo clippy -p openfang-{runtime,kernel,api} --all-targets
      -- -D warnings`  → clean
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant