Channel execution events¶
Channels provide both connector operations and optional execution notices.
Channel.actions advertises operations exposed by the existing
ChannelRuntime tools: channel_list, channel_send, and channel_action.
async Channel.on_event(event: ChannelExecutionEvent) -> None adds a separate
status hook. Its default implementation is a no-op, so existing connectors work
without modification. Channel.activity(ChannelActivity) remains the typing API.
Import the new API from nagents.channels:
from nagents.channels import ChannelExecutionEvent
from nagents.channels import ChannelExecutionPhase
from nagents.channels import dispatch_channel_execution_event
from nagents.channels import sanitize_channel_tool_arguments
Contract¶
ChannelExecutionEvent is a frozen dataclass with required conversation_id,
session_id, and phase; optional thread_id, run_id, activation_id,
call_id, tool_name, and message_id default to empty strings.
destination is a read-only alias of conversation_id.
tool_arguments: dict[str, ChannelValue] defaults to {} and
tool_failed: bool defaults to False.
ChannelExecutionPhase is a literal union:
| Phase | Meaning / standalone Agent.listen mapping |
|---|---|
run_started |
An inbox activation was claimed, before model work starts. |
tool_requested |
ToolCallEvent observed; execution and approval are not established. Includes the call ID, tool name, and sanitized arguments. |
tool_completed |
ToolResultEvent observed; the executor returned a result. Includes the call ID and name, and tool_failed=True when an error result was returned. Arguments are empty; no result/error payload is included. This does not prove a remote side effect succeeded. |
waiting_for_approval |
Available to hosts with approval gates; standalone listening does not manufacture this state. |
completed |
The run, generator/plugin cleanup, and inbox finalization succeeded. A raw DoneEvent alone does not establish completion. |
failed |
Provider error, execution/cleanup exception, or failure of the existing local observer. Tool error results alone need not fail a run. |
cancelled |
The activation was interrupted by cancellation. Requested tools may have unknown outcomes; no replay is implied. |
Standalone listening supplies a unique channel-run-<uuid> run ID and
channel-inbox:<row-id> activation ID (scoped to the inbox database/session),
plus the ingress message ID. Each activation's notices use only its incoming
connector, conversation, and thread. Explicit cross-channel tool sends do not
change that notice route. All attached conversations still share one serialized
agent session. Older inbox envelopes without routing information execute without
notices rather than guessing a destination.
The existing ChannelEvent / Agent.listen(on_event=...) API is still the local
raw-event observer; it is distinct from the connector hook, including its
existing exception semantics. Final assistant text is never automatically sent
through either the new hook or channel tools.
Privacy and delivery¶
Events have no raw-event, assistant-text, reasoning, prompt, tool-result, or exception-text field. Construction sanitizes arguments; dispatch sanitizes a detached copy again to protect against mutable dictionaries.
String display contract: ordinary string values of at most 80 Unicode
characters remain visible in full when they are printable, single-line, and
free of recognized credential patterns. For example, a read_file request with
{"path": "README.md", "start_line": 20} retains both arguments, allowing the
connector to display path=README.md, start_line=20. Short names, queries and
commands such as python -m pytest -q are also preserved.
Sensitive-key subtrees are redacted, even when their contents appear ordinary.
The filter also redacts common service/API token prefixes, JWTs, Bearer/Basic/
Digest authentication, URL userinfo, credential assignments and command flags,
including embedded and common percent-encoded forms. Keys are screened too.
Every candidate that can be displayed is checked in full, including up to two
percent-decoding passes; controls and encoded line breaks are rejected. Long
strings are omitted outright as [redacted], without scanning or retaining a
prefix. No string value is ever prefix-truncated, so a secret cannot become
a seemingly ordinary displayed prefix through length truncation.
This is conservative display filtering, not guaranteed detection of arbitrary
secrets under misleading keys. The core has no application credential state.
Before constructing an event, a host must check full original arguments
(including keys and values) against its saved-credential guard, and redact or
omit matches. In the web host this is its CredentialGuard. A connector's own
formatter must additionally check its own credentials before rendering. These
host checks must use full originals, not shortened or already-filtered summaries;
connector checks apply to the received values before any formatter shortening
or interpolation.
Short identifier keys, finite numbers, booleans and null may remain. Summaries retain their depth-4, 64-node, 16-input-entries-per-container, and 4096-JSON-byte limits. Invalid objects are redacted without calling their string representation. Treat summaries as display data, never executable tool inputs. Argument names, values and correlation IDs are untrusted display data; a connector must escape them for its transport.
await dispatch_channel_execution_event(channel, event, *, timeout=2.0) attempts
the optional hook once. Exceptions, timeout, and connector self-cancellation are
isolated and their text is not logged or forwarded. It does not retry uncertain
notice sends. Owner cancellation waits for the bounded attempt to finish and
then propagates, even under repeated cancellation. Dispatches are joined, with
no background queue or orphaned task. Connector hooks must cooperate with asyncio
cancellation and keep their transport I/O bounded. The helper skips invalid
routes, but does not establish authorization or durable session ownership.
Web host and connector integration¶
- The host resolves and verifies the durable session owner, including its connector, conversation, and thread, before dispatch. Never infer ownership from a tool's outbound destination, a last-active chat, or an attached-channel broadcast. Supply the host's durable activation/run/call IDs when available.
- Check full original arguments against the host's saved credentials, then
construct
ChannelExecutionEventusing the mapping above. Emit approval state only at a real host approval gate. Emit terminal state after owned execution cleanup; do not equate a requested tool with successful execution. - Await
dispatch_channel_execution_event(channel, event)in the owned run or cleanup task, and join dispatches before closing the connector. If a host owns a queue, it also owns its ordering, bounds, ownership rechecks, and shutdown. - A connector overrides
on_eventto render concise status text or edits usingevent.destinationandevent.thread_id. Keep its existing actions and typing implementation. Check its own credentials, make at most one transport attempt, escape display data, and avoid forwarding raw model content. Existing connectors can keep the no-op until their next release.