Repository navigation
Hosted channels: generic provider behaviour (provider table, answer and session modes) - #603
alex-clickhouse wants to merge 10 commits into
Conversation
|
Nerve PR review · ✅ Ready to merge Updated for 22fb672: another rebase onto main This is the base that #601 (email) stacks on. It replaces the hard-coded Slack list with a provider table ( Completeness: a behaviour suite, including a real engine that stores a tool-mode answer; contract tests with a pinned sample; Worth considering (both matter once #601 adds tool mode):
Checked: full suite in an offline sandbox at |
8489a28 to
a27f301
Compare
|
Rebased onto main d87526e (8489a28 -> a27f301, the same 10 commits). #601 is restacked on it. Conflicts and how they were merged:
Checked: hosted, channel and contract tests plus test_slack_channel, test_session_origin, test_drain, test_send_file: 1231 passed. Full backend suite on #601's tip (includes this branch): 8681 passed, 38 skipped (= 8719 collected), test_slack_integration.py excluded. |
The gateway can now announce, in the capabilities frame, which provider a connection belongs to and how Nerve answers its events: post the answer or let the agent answer with a tool, and rotate or keep the conversation's session. A hosted channel needs both to serve a provider other than Slack. Absent members keep today's meaning, so an older gateway changes nothing. The checks follow the gateway's contract, and the contract's new sample is copied byte for byte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hosted runtime built a channel only for Slack, and the channel read Slack-shaped settings and a Slack-shaped switch from the provider's config section. A provider without such a section had a channel that was never switched on, which stopped the inbox reader for every provider. A table of hosted chat providers now gives each provider its hosted mode, its switch, its channel settings with defaults, and its presentation rules. Slack is the only entry and keeps its behaviour, including the pause of intake while it is switched off. A provider added later rejects its events while it is off, so it never stops the reader. A channel uses only the connections of its own provider when the gateway names the provider, for the choice of connection, limits, availability, notify, and addressed delivery. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A provider such as email has no conversation that Nerve can post a turn's answer to: the agent answers only with a tool. The router always showed typing, streamed a placeholder, and posted the answer or an "Error:" message, so such a provider needed a way to stay silent. An inbound message now says whether its turn posts its answer. When it does not, the router sends nothing to the channel for the turn, and the answer stays in the session. A hosted channel takes this from the behaviour that the event's connection announced, and puts one generic line before the turn's text: where the message came from, that the answer goes to no one, and which tool can answer. A connection without an announced behaviour posts, so Slack does not change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A conversation of a provider such as email is one thread that can go quiet for days, and Nerve's sticky period moved its next message into a new session after two idle hours. The agent then lost the thread's context. A connection that announces the keep session mode now sends each turn of a conversation to that conversation's last session, however long it was idle, while the session is not archived or deleted. Rotate, and a connection that announces nothing, keep the sticky period. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hosted mode docs now say that a channel uses only the connections of its own provider, what the answer and session modes of a connection do, the line that Nerve puts before a turn in tool mode, and what a switched-off provider does with its events. Hosted Slack still holds the events of every provider while it is off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A provider's switch need not be a key named after the provider, so the log detail of a rejected event says only that the provider is off. The runtime docstring also says that a provider's switch and settings follow a reload. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two messages of a new conversation that arrived together could each find no session and create one, so one conversation was split across two sessions. With the keep session mode this breaks the rule of one session per conversation; in rotate mode it was a race as well. The router now resolves the session and queues the message under a lock per channel key. The second message then finds the session that the first one created, and the queue keeps the arrival order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A channel remembered the connection of every accepted event and used it for replies, and it read the behaviour of that connection, without a check of the connection's provider. An event of one provider on a connection of another, from a gateway fault or a changed announcement, could thus take the other provider's behaviour and send through its connection. A channel now rejects an event on a connection whose capabilities name another provider, and it sends no operation through such a connection: a remembered connection that now serves another provider is skipped, and the channel chooses among its own connections. A connection that names no provider still serves every channel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The inbox of the gateway is one ordered queue, so Nerve reads no event of any provider while one hosted channel has its maximum of turns in progress. The hosted mode docs now say so where they describe the switched-off hold. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The copy of the contract's capabilities sample must stay byte for byte the same as the gateway's, so a test checks its SHA-256. A test with a real engine, whose model process is a stand-in, shows that the answer of a tool turn is stored in the session while nothing is posted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
a27f301 to
22fb672
Compare
Stack (merge in this order): this PR (generic hosted channel behaviour) -> #601 the email channel. The channel gateway that sends the new capabilities members and the email events ships first.
What
capabilities:
providerandbehaviour(answer:postortool,reply_tool,session:rotateorkeep). A copy of the contract'snew sample
capabilities_behaviour.jsonis intests/fixtures/channel_contract/.nerve/channels/hosted/providers.py)replaces the fixed Slack list. Each entry gives the provider's hosted mode,
its switch, its channel settings, and what its channel does while the
provider is switched off. Slack is the only entry.
gateway names the provider.
provider and sends no operation through such a connection.
time, so simultaneous first messages share one session.
toolmode, Nerve posts nothing for a turn: no typing mark, nostreamed placeholder, no answer, and no "Error:" message. The answer stays
in the session. Nerve puts one line before the turn's text that says where
the message came from, that the answer goes to no one, and which tool can
answer.
keepmode, a conversation continues its last session, however longit was idle, while that session is not archived or deleted.
How
ConnectionBehaviourand the newCapabilitiesmembers follow thegateway's checks. Absent members have the defaults
post, no tool, androtate, so an older gateway changes nothing.HostedChatProviderentries givehosted,switched_on,settings(
HostedChannelSettings, with defaults for a provider without a Slacksection),
hold_while_off, andchannel_options. Slack holds its eventswhile it is off, as before. Any other provider rejects its events while it
is off (
configuration_invalid), so it never stops the inbox reader.StreamManager.connections()andOperationRunner.connections()take anoptional provider.
InboundMessage.post_answertells the router to skip all channel outputfor a turn. The hosted channel sets it from the behaviour of the event's
connection and, for
keep, setssession_idto the conversation's lastsession.
Why
A provider such as email has no conversation that Nerve can post to, its
conversations can be idle for days, and it has no Slack-shaped settings.
Before this change, Nerve could only serve Slack: a second provider stopped
the inbox reader, shared Slack's connections, and got automatic answers and
new sessions after two idle hours. Email is added on top of this in a
separate PR.
Notes
events of every hosted provider, as it holds GitHub records today, and a
hosted channel with 100 turns in progress pauses the reading for every
provider; both are in
docs/config.md.Nerves ignore them.
Tests
New
tests/test_hosted_channel_behaviour.py(a second provider without aconfig section, refuse while off, scoping with two providers, a connection
of another provider, tool mode posts nothing and a real engine stores the
answer, the note line, keep vs rotate, simultaneous first messages, router
batches) and contract tests, including the fixture's SHA-256. Existing hosted channel tests pass unchanged.
🤖 Generated with Claude Code