Repository navigation
feat(prose): add a 'named' prose routing mode, which posts no plain text and needs a channelId for channel_publish - #868
Draft
nissa-seru wants to merge 1 commit into
Conversation
A new proseRouting value, 'named', that an agent opts into: its plain text is never posted to any channel, and nothing the framework sends for it falls back to a default destination. Existing recipes are unmoved, since no other mode changes. Its turns route as 'disabled' does (turnModeOf), so every publication path posts none of its plain text, >> and >>> lines and heartbeat ticks included, with no branch at any of them; it gets no primer. Its completed speech isn't handed to modules' speech handlers either, which can post it (the bundled Discord module does). The sleep announcement reads the configured mode, so it gains the one branch: nothing is announced. Every request for its turns carries one constant line at the system position, beside the subconscious's dispositions, so the rule survives a fold and an allowedTools that leaves its tools out. The line names channel_publish only where the agent may call it, and tells a subagent (an ephemeral run) that its final reply goes back to whoever started it. Its tools agree with the rule: channel_publish requires channelId and promises no default, skip_reply says a reply is sent with a tool that names where it goes, think drops the same-round routing sentence, and sleep says nothing is announced and drops announce and message. No other agent's tool list changes. Its channel_publish with no channelId is refused in the registry (namesItsChannels), where every agent-origin publish passes, a puppeted or script call included; a named conversation fork's such call skips the home fill, so it's refused there too. A named fork's drafts resend with no destination is refused, not sent home. Its receipt says the segments were kept in its context and sent to no one, and a provider's refusal of its turn is marked on no message, since the latest inbound message isn't a destination it named. A closed-channel invitation offers channel_publish with the channelId that reaches that channel alone (publishNameFor, which publishRouteFor now uses too), where one does and the agent may call it, and says nothing about plain text, so its words hold for any recipient of the shared message that can call channel_publish. The NOT routed log lines say why: a silent turn, or the configured mode.
nissa-seru
marked this pull request as draft
October 28, 2026 02:55
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This adds an opt-in
proseRouting: 'named'. A named agent's plain text isn't posted to a channel or handed to a module's speech handler, and itschannel_publishwith nochannelIdis refused instead of posting to a default channel. Every request for its turns carries a line at the system position that says so and names how to speak, the same line in each of the agent's requests. Its tools and its receipt say the same, and its closed-channel invitation offerschannel_publishwith an id that reaches the channel, where one does and the agent may call it. For the four existing modes, a probe ofdisabled,locus,hybridandexplicitagents gave byte-identical output atmainand at this head.Problem
Two reports describe an agent's plain text going where the agent didn't choose. anima-research/connectome-host#96 reports internal continuity summaries routed as ordinary public chat when the current chat locus was sticky, and #551 reported subagents publishing their plain text to the global fallback channel.
In the probe of the four modes that Tests describes, at
maine85c57db9a, thedisabledagent posted none of its plain text. But itschannel_publishwith nochannelId, made in a heartbeat tick, posted todisc:guild:other, the channel the run's earlier messages came in on. Changingdisabledwould change what existingdisabledagents meet, so this adds a mode instead.Changes
The mode.
'named'joinsAgentConfig.proseRoutingandAgent.proseRouting, with a doc comment insrc/types/agent.ts.Plain text. A named agent's turn routes as a
disabledturn does (turnModeOf,src/framework.ts:364), and gets no primer. Its completed speech and thoughts aren't dispatched to modules' speech handlers (:23049). Itssleepcall posts nothing.The system line. For a named agent, each turn adds a system-position injection in the
prose-routingnamespace (:21353), fromnamedProseLine(:353). It reads, for an agent that may callchannel_publish:An agent that may not call
channel_publishis pointed to "a tool that names a channel, a person or a message" instead. For a subagent (an ephemeral run), the second sentence says its final reply goes back to whoever started it and other plain text stays in its own context.Its tools.
channel_publishrequireschannelId, and its description says nothing is sent to a default channel.skip_reply's description says to reply with a tool that names where it goes.thinkdrops the sentence about same-round routing.sleepsays nothing is announced, and dropsannounceandmessage.Other agents' descriptions are built from the same strings as before.
The refusal. The channel registry refuses an agent-origin
channel_publishwith nochannelIdfor a named agent, through anamesItsChannelsoption the framework passes (src/mcpl/channel-registry.ts:6157,src/framework.ts:29076). The refusal reads "Nothing was posted: channel_publish needs a channelId, since nothing is sent to a default channel. channel_list shows the channel ids." A named conversation fork'schannel_publishwith nochannelIdis refused too, not sent to the fork's home (:33227).A fork's
draftsresend with no destination is refused for a named agent, instead of going to the fork's home (:18119).The receipt after a turn with plain text says how many plain-speech segments were "kept in your context, sent to no one (only a send tool that names where it goes reaches anyone)".
A refusal by the provider of a named agent's turn gets no emoji reaction on the latest inbound message (
reactToRefusal,:11173).The closed-channel invitation's first option, for a named agent that may call
channel_publish, is achannel_publishcall with thechannelIdthat reaches that channel alone. Otherwise it says to check whether the server exposes a send tool that can address it.publishNameForis taken out ofpublishRouteFor, which now calls it.Log lines. The
NOT routedlines say why: a silent turn, or the configured mode.changelog.d/named-prose-routing.added.md, new.test/named-prose-routing.test.ts, new, with 13 tests.Tests
Each of the 13 tests drives a named agent. Several also check that another agent meets what it did before.
>>and>>>lines included, and no primer is given;sleepcall posts nothing, and it's told of no undeliverable reply.Membranewith membrane's Anthropic adapter building each wire request;channel_publishonly where the agent may call it;channel_publish:channelIdit's refused and posts nothing, with aserverIdalone, with an emptychannelId, from a script, and as an operator's puppeted call;channel_publishnaming the channel.reactToRefusal, called directly) adds no emoji reaction for a named agent, while it still adds one for alocusagent.channel_publishordraftsresend with no destination is refused, not sent to its home.On Linux with Node 24.22.1, context-manager 0.23.1, membrane 0.8.0 and chronicle 0.4.0, in a clean checkout of
e7b5489b90:tscclean, then 8,806 tests, 8,744 pass, 0 fail, 62 skipped.e85c57db9a'ssrc/framework.ts,src/mcpl/channel-registry.ts,src/agent.tsandsrc/types/agent.ts: all 13 new tests fail.The four existing modes. A probe drives a
disabled, alocus, ahybridand anexplicitagent through the same steps, among them a plain reply,>>and>>>replies, and a heartbeat tick that callschannel_publishwith nochannelId. Its output records what each step posted, the window's rows, and the system and tool fingerprints of each of 21 requests. Run under Bun 1.4.4 with the same packages, atmaine85c57db9aand ate7b5489b90, the two outputs are byte-identical.With other open pull requests
3a24c4728f, merges with this change without conflicts. On that merge, on Linux with Node 24.22.1, context-manager 0.23.1, membrane 0.8.0 and chronicle 0.4.0,tscis clean, the 13 new tests pass, andtest/prose-drafts.test.tspasses 253 of 253. feat(prose)!: a heartbeat turn holds its plain speech as drafts #861 raises its heartbeat notice insideif (turnProseRouting !== 'explicit' && turnProseRouting !== 'disabled')(src/framework.ts:21211at3a24c4728f), and this change'sturnModeOfgives'disabled'for a named agent (src/framework.ts:364).ed4ddb7167, merges with this change without conflicts. On that merge, on Linux with Node 24.22.1, context-manager 0.23.1, membrane 0.8.0 and chronicle 0.4.0,tscis clean, the 13 new tests pass, and fix(tune-out): the default channel skips channels that are tuned out #863'stest/tune-out-default-channel.test.tspasses 8 of 8.Not verified
channel_publishfor a named agent, acallToolwith module origin, isn't tested. The registry's refusal applies whereorigin?.kind === 'agent'(src/mcpl/channel-registry.ts:6157), and a module'scallToolruns the call with{ kind: 'module' }as its origin (src/framework.ts:4092).const invited = woken ?? this.deliveryAgent();,src/framework.ts:15702). No test has residents in different modes reading one window.previewActivationfor a named agent isn't tested.changelog.d/named-prose-routing.added.md.Hamish-2699 made the change, from a design by Ada-1017 that Florence-3585, Imogen-2490, Naomi-2422 and Jerome-2511 reshaped. Daniel-2612 wrote this description.