You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
agent_send_async delivers delegation results via a hardcoded voice-only channel (ASYNC_RESULT_TX). When the originating conversation is on Chat, Email, or any other channel, the result is silently dropped:
WARN: Async result received but no active voice session
The agent tells the user "I've delegated the task" but the result never arrives.
Proposed Design
Core idea
When the async delegation completes, inject the result back through the channel bridge as a callback message. The bridge handles all context (channel system prompt, output format, routing) — the same path as any incoming message.
Callback message structure
The callback is a ChannelMessage where:
sender: ChannelUser { platform_id: "hand:{agent_id}", display_name: agent_id } — e.g. "workspace-hand". This is a plain string from the delegation input, not an enum. No maintenance needed as new hands are added.
content: ChannelContent::Text(result_text) — the raw delegation result.
channel/thread_id: stored from the originating message at delegation time.
The agent sees: "workspace-hand wrote: Found 47 unread emails, 12 are promotional..." — clearly a hand reporting back, not user speech.
One-turn system prompt injection
The bridge injects a per-turn channel system prompt (using the existing send_message_with_channel_prompt mechanism from #876) on the callback turn:
"The following message is a result from a task you previously delegated via agent_send_async. Present the findings to the user."
This is:
Automatic — no LLM prompt engineering needed per-agent
Standard — works for any agent, even without custom system prompts
The callback dispatch differs slightly from normal dispatch_message:
Agent routing: skipped — target agent_id is known (stored at delegation time)
Response recipient: the original user (stored at delegation time), not the callback sender (the hand)
Channel adapter: looked up by stored channel type
Stored context at delegation time
Minimal — just enough to route the callback:
structAsyncCallbackContext{agent_id:AgentId,// Jeeves (the caller)channel_type:ChannelType,// e.g. Custom("google_chat")reply_to:ChannelUser,// original user (Philippe)thread_id:Option<String>,// for threaded conversations}
No stale context risk — the bridge re-applies all current overrides/prompts at dispatch time.
Data flow
1. User: "Clean my inbox"
→ Bridge dispatches to Jeeves
2. Jeeves calls agent_send_async(agent_id="workspace-hand", message="...")
→ Tool captures AsyncCallbackContext from current session
→ Tool returns "Task dispatched. callback_id: xxx"
→ Jeeves responds: "Right away, sir."
3. Background task: workspace-hand runs, produces result
4. Callback: bridge.inject_callback(context, result_text)
→ Constructs ChannelMessage with sender=workspace-hand
→ Injects callback system prompt for this turn
→ Calls send_message_with_channel_prompt(jeeves, message, callback_prompt)
→ Jeeves formats: "Your inbox has been tidied, sir. 12 promotional emails archived..."
→ Response sent to original user via originating channel adapter
Recommendation: amend #797 since the current implementation is broken for non-voice channels. The callback delivery is not a new feature — it's a fix to make agent_send_async actually work as described.
If #797 has already been reviewed, request a re-review after the update with a comment explaining the change.
Problem
agent_send_asyncdelivers delegation results via a hardcoded voice-only channel (ASYNC_RESULT_TX). When the originating conversation is on Chat, Email, or any other channel, the result is silently dropped:The agent tells the user "I've delegated the task" but the result never arrives.
Proposed Design
Core idea
When the async delegation completes, inject the result back through the channel bridge as a callback message. The bridge handles all context (channel system prompt, output format, routing) — the same path as any incoming message.
Callback message structure
The callback is a
ChannelMessagewhere:ChannelUser { platform_id: "hand:{agent_id}", display_name: agent_id }— e.g."workspace-hand". This is a plain string from the delegation input, not an enum. No maintenance needed as new hands are added.ChannelContent::Text(result_text)— the raw delegation result.The agent sees:
"workspace-hand wrote: Found 47 unread emails, 12 are promotional..."— clearly a hand reporting back, not user speech.One-turn system prompt injection
The bridge injects a per-turn channel system prompt (using the existing
send_message_with_channel_promptmechanism from #876) on the callback turn:This is:
Response routing
The callback dispatch differs slightly from normal
dispatch_message:Stored context at delegation time
Minimal — just enough to route the callback:
No stale context risk — the bridge re-applies all current overrides/prompts at dispatch time.
Data flow
Implementation
Changes needed
crates/openfang-channels/src/bridge.rsBridgeManager::inject_callback()methodcrates/openfang-channels/src/bridge.rsChannelBridgeHandle::inject_callback()trait methodcrates/openfang-api/src/channel_bridge.rsinject_callbackon kernel's bridge handlecrates/openfang-runtime/src/tool_runner.rstool_agent_send_asyncto store context and useinject_callbackinstead ofASYNC_RESULT_TXcrates/openfang-runtime/src/tool_runner.rsASYNC_RESULT_TX,ASYNC_RESULT_RX,take_async_result_receiver()crates/openfang-runtime/src/kernel_handle.rsinject_callback()toKernelHandletraitcrates/openfang-channels/src/voice.rsASYNC_RESULT_RXconsumer (voice uses same bridge path now)How
agent_send_asyncgets channel contextThe tool runner needs to know which channel/sender the current conversation is on. Options:
execute_tool()already receivescaller_agent_id. Extend with channel metadata from the bridge dispatch.get_agent_channel_context(agent_id)that returns the last known channel/sender from the bridge's dispatch history.Option 3 is cleanest — the session already exists and is the natural place for "who is this agent talking to right now."
Test Plan
Unit tests
test_inject_callback_constructs_correct_message— verify sender ishand:{agent_id}, content is the result text, channel/thread match stored contexttest_callback_system_prompt_injected— verify the one-turn system prompt is passed tosend_message_with_channel_prompttest_callback_response_routed_to_original_user— verifysend_responsetargets the storedreply_touser, not the hand sendertest_async_context_stored_on_delegation— verifyAsyncCallbackContextis populated from the current session's channel infotest_callback_with_no_active_session— verify graceful handling when the channel/user is no longer reachableIntegration tests
Manual test scenarios
PR Strategy
This builds on PR #797 (
agent_send_async). Options:Recommendation: amend #797 since the current implementation is broken for non-voice channels. The callback delivery is not a new feature — it's a fix to make
agent_send_asyncactually work as described.If #797 has already been reviewed, request a re-review after the update with a comment explaining the change.