Repository navigation
feat(prose): add a 'named' prose routing mode, which posts no plain text and needs a channelId for channel_publish - #868
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.
|
/review |
About these findingsThe findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits. Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase. Automatic review of Findings, most severe first:
|
|
/context-deltas |
|
Automatic context view of What a context view runs and what its checks judge: CHECKS.md. This head couldn't be compared: it doesn't merge onto |
Two reports, anima-research/connectome-host#96 and #551, describe an agent's plain text going where the agent didn't choose. At
mainof October 28,e85c57db9a, no prose routing mode kept it from every channel: underdisabled, plain text still went to modules' speech handlers, and the framework's own Discord module posts a text-only turn's text, and achannel_publishwith nochannelIdwent to a default channel. This adds an opt-in mode,proseRouting: 'named', in which an agent's plain text goes to no channel and no speech handler, and itschannel_publishmust name a channel.An agent whose plain text should go nowhere, and whose publishes should name their channel
mainate85c57db9a): no mode does both.disabledsends none of the agent's plain text through the channel registry, but hands it to modules' speech handlers, and the framework's own Discord module posts a text-only turn's text to its reply context's channel. Itschannel_publishwith nochannelIdposts to a default channel: in a probe, a heartbeat tick's call went to the channel the run's earlier messages came in on.namedagent's plain text is never posted to a channel or handed to a module's speech handler, and itschannel_publishwith nochannelIdis refused: "Nothing was posted: channel_publish needs a channelId, since nothing is sent to a default channel. channel_list shows the channel ids." Each of its requests carries a system line saying so, and its tools and its receipt say the same. For a channel whose server hasn't declared it inbound-only, its closed-channel invitation offerschannel_publishwith an id that reaches that channel alone, where there is one and the agent may callchannel_publish, and otherwise says to check whether the server exposes a send tool that can address the channel.For the four modes
mainhad ate85c57db9a,disabled,locus,hybridandexplicit, a probe of an agent in each gave byte-identical output there and at this head.mainhas since added a fifth,'locked'. By its doc, its unprefixed prose goes to one destination the agent chose withprose_locus, or to the host'sproseLockuntil it chooses, and a module that handles speech itself still receives it, as in every mode. Neither the probe nor the tests ran at today'smain.What it leaves: the framework's own Discord module isn't changed, so a named agent's
discord--replystill goes to the module's reply context, a channel the call doesn't name. A module's ownchannel_publishfor a named agent isn't refused, by reading, and no test runs it. The closed-channel invitation is written for one agent, the woken one or else the delivery agent, so in a window residents in different modes share, a named resident can read one written for another mode, which can ask for a reply in plain text or with a>>or>>>prefix, and following that posts nothing. No test runs a switch into named mode orpreviewActivationfor a named agent, and no run used a live provider.It stays a draft until a round of ours brings it onto
mainas it is now and answers its automatic review.changelog.d/named-prose-routing.added.md.Credits
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 the description, and Ramona-2416 put it in this shape.
Details for agents
Head
e7b5489b9031, intomain. Its probe and runs are atmaine85c57db9a, of October 28, andmainis now0c570d8f2d.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 modes that existed on October 28, a probe ofdisabled,locus,hybridandexplicitagents gave byte-identical output at that day'smain,e85c57db9a, and 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.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). An invitation written for alocusagent can ask for a reply with no prefix (src/framework.ts:15708at this head), and one for ahybridorexplicitagent for one prefixed>>>or>>(:15726); a named agent's plain text, prefixed lines included, posts nothing. No test has residents in different modes reading one window.previewActivationfor a named agent isn't tested.main's fifth mode.mainhas since addedproseRouting: 'locked'(src/types/agent.ts:344at0c570d8f2d, wheree85c57db9a's:341has four modes). Its doc there (:333–343) says unprefixed prose goes to one destination the agent chose withprose_locus, orproseLockuntil it chooses, and that a module that handles final speech itself, such as the in-process Discord module, receives it in every mode. That's a reading of the doc; neither the probe nor the tests ran at0c570d8f2d.src/modules/discord/index.ts. Ate85c57db9ait registers as the speech handler for every agent (:446), which is how adisabledagent's plain text reaches it: the speech dispatch's condition names no mode (src/framework.ts:22964). Itsreplytool requires onlycontent(:590–599at this head), and sends to its reply context's channel (:1579–1581), so a named agent'sdiscord--replyreaches a channel the call doesn't name. No test here runs the module.