What problem are you trying to solve?
An agent running from an interactive channel can currently rely on its terminal assistant response being delivered to that channel, but it has no channel-neutral way to intentionally send an additional user-visible message before the turn ends.
This matters for long or multi-stage turns. An agent may need to tell the user that a result is available, report a meaningful milestone, or explain that it is continuing with follow-up work, then execute more tools before producing its terminal response.
Assistant text emitted before a tool call is not an adequate send primitive. Whether that text is delivered is an implicit adapter policy rather than an explicit model action. For example, Slack's default handler turns pre-tool narration into status text. Overriding message.completed is transport-specific, couples delivery to model-step boundaries, and can accidentally replace the built-in terminal reply handler, as described in #623.
Making all delivery explicit is also unsafe: if the model forgets to call a send tool, it can finish a successful turn with an answer that is silently lost. The framework needs separate, unambiguous contracts for an additional mid-turn update and the existing terminal reply.
Proposed solution
Add a framework-owned, channel-neutral tool available in interactive root sessions whose active channel supports outbound messages:
send_channel_update({
message: "The export is ready. I’m updating the project wiki now.",
});
The contract should be deliberately narrow:
- The tool sends one additional message immediately to the active conversation. It does not address an arbitrary channel or recipient.
- Calling it never replaces, satisfies, or suppresses terminal delivery. When the turn ends, the terminal assistant response is still delivered by the channel exactly as it is today.
- It may be called more than once in a turn.
- It is exposed only when the active channel declares that it can perform an interactive outbound send, and only to the root session by default. It should be absent from schedules, task-mode runs, and delegated agents that cannot address the user directly.
- Delivery is durable and idempotent by tool call id so workflow retries do not duplicate user-visible messages.
- The call and delivery outcome are distinct in observability from ordinary tools and from terminal assistant delivery.
Use a name such as send_channel_update, rather than send_channel_message or send_slack_message, to encode both important constraints: this is an additional update, and it targets the active channel generically.
The model-visible tool description should state the terminal behavior directly:
Send an additional message to the user now in the active conversation, then continue the current turn. This does not replace your final response: when the turn ends, your final assistant message will still be delivered automatically. Do not use this tool for your final answer.
The framework's channel-session instructions should reinforce the same invariant:
Your final assistant message is automatically delivered to the active channel. send_channel_update adds an earlier message; it never replaces the final delivery.
The tool result should repeat it concisely after each call:
Update delivered. Continue working; your final response will still be delivered automatically.
At the channel API layer, add an optional imperative outbound operation to the active adapter and use that capability to decide whether the tool is present. Built-in Slack, Discord, Telegram, Teams, Linear, GitHub, and Chat SDK channels can project the generic text input into their existing reply operation. Channels without the operation retain current behavior and never expose the tool.
Related issues:
Alternatives considered
Override events["message.completed"]. This posts text only when the model happens to create another assistant step, cannot express an intentional send independently of a tool boundary, is channel-specific, and replaces framework delivery behavior unless defaults are manually reconstructed. See #623.
Add send_slack_message. This solves one transport while leaking Slack credentials and thread identity into the model tool surface. The intended operation is replying in the active conversation, which every interactive channel can implement.
Require explicit tool calls for every user-visible message, including the final response. A model that forgets the call would complete successfully while the user receives nothing. You could add additional heuristics to to restart a turn that forgot to send a message before ending, but retaining automatic terminal delivery provides a simple & deterministic safety net.
Suppress terminal delivery whenever the tool was called. A progress update could then accidentally consume the only delivery and silently discard the actual answer. The update tool must be additive.
Infer delivery from assistant text, finish reasons, message phase, or timing. These are useful rendering signals but do not express model intent. A tool call is explicit, observable, testable, and portable across providers.
What problem are you trying to solve?
An agent running from an interactive channel can currently rely on its terminal assistant response being delivered to that channel, but it has no channel-neutral way to intentionally send an additional user-visible message before the turn ends.
This matters for long or multi-stage turns. An agent may need to tell the user that a result is available, report a meaningful milestone, or explain that it is continuing with follow-up work, then execute more tools before producing its terminal response.
Assistant text emitted before a tool call is not an adequate send primitive. Whether that text is delivered is an implicit adapter policy rather than an explicit model action. For example, Slack's default handler turns pre-tool narration into status text. Overriding
message.completedis transport-specific, couples delivery to model-step boundaries, and can accidentally replace the built-in terminal reply handler, as described in #623.Making all delivery explicit is also unsafe: if the model forgets to call a send tool, it can finish a successful turn with an answer that is silently lost. The framework needs separate, unambiguous contracts for an additional mid-turn update and the existing terminal reply.
Proposed solution
Add a framework-owned, channel-neutral tool available in interactive root sessions whose active channel supports outbound messages:
The contract should be deliberately narrow:
Use a name such as
send_channel_update, rather thansend_channel_messageorsend_slack_message, to encode both important constraints: this is an additional update, and it targets the active channel generically.The model-visible tool description should state the terminal behavior directly:
The framework's channel-session instructions should reinforce the same invariant:
The tool result should repeat it concisely after each call:
At the channel API layer, add an optional imperative outbound operation to the active adapter and use that capability to decide whether the tool is present. Built-in Slack, Discord, Telegram, Teams, Linear, GitHub, and Chat SDK channels can project the generic text input into their existing reply operation. Channels without the operation retain current behavior and never expose the tool.
Related issues:
message.completedis not a suitable application-level substitute.message.completedmay fire multiple times per turn, reinforcing that model-step events and intentional user delivery are different abstractions.Alternatives considered
Override
events["message.completed"]. This posts text only when the model happens to create another assistant step, cannot express an intentional send independently of a tool boundary, is channel-specific, and replaces framework delivery behavior unless defaults are manually reconstructed. See #623.Add
send_slack_message. This solves one transport while leaking Slack credentials and thread identity into the model tool surface. The intended operation is replying in the active conversation, which every interactive channel can implement.Require explicit tool calls for every user-visible message, including the final response. A model that forgets the call would complete successfully while the user receives nothing. You could add additional heuristics to to restart a turn that forgot to send a message before ending, but retaining automatic terminal delivery provides a simple & deterministic safety net.
Suppress terminal delivery whenever the tool was called. A progress update could then accidentally consume the only delivery and silently discard the actual answer. The update tool must be additive.
Infer delivery from assistant text, finish reasons, message phase, or timing. These are useful rendering signals but do not express model intent. A tool call is explicit, observable, testable, and portable across providers.