Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog.d/publish-for-agent-shared-id.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- `PublishRefusalReason` gains `shared-id`: `publishForAgent` refuses a name
that means more than one registered channel, before anything goes out. A
ref picks one, as a server-qualified name does where it names one channel
alone.
38 changes: 38 additions & 0 deletions changelog.d/shared-channel-ids.breaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
- **Code that calls `ChannelRegistry` or `ConversationRouter` directly:** a
channel the registry acts on is a `ChannelRef`, `{ serverId, channelId }`
(exported as a type), where it was a bare channel id, since two MCPL
servers can register the same id.
- `routeSpeech` and `deliverSpeech` take the locus as a `ChannelRef` (or
`null`), and `resolveLocus` and `getDefaultPublishChannel` return one.
`getDescriptor`, `isChannelOpen`, `inboundOnlyServer` and
`proseTargetFor` take one. `getChannelServerId` is removed: a ref names
its server.
- `buildChannelContext(agentName, serverId)` builds the channel context one
server is told, and `incomingFor(agentName)` gives the message the agent
is replying to, with its channel.
- `ChannelRegistryOptions.homeChannelResolver` and `activeChannelResolver`
return a `ChannelRef`.
- `sendOutgoingChunk` and `sendOutgoingComplete` take only the
destination the publish went to. A bare channel id, which they resolved
again at each call and dropped where two servers register it, is no
longer taken.
- `ConversationRouter`'s `bind`, `touch`, `unbind` and `getBinding` take a
`ChannelRef`, `channelForAgent` returns one, and a `ConversationBinding`
carries `serverId`.
- **Migration:** pass `{ serverId, channelId }`. An inbound event, a
publish outcome's `destination` and `channel_list` each carry the server.
`resolveProseTarget`'s answer, `openIfClosedForSend`'s, `channel_open`'s
result data, a gate wake's `WakeProvenance` (as `routeServerId`) and
`onRouteFailure`'s info now carry it too, and `InferenceRequest` carries
it as `channelServerId`, beside `channelId`. A stream's chunk and
`complete` take the publish outcome's `destination`. For a name given as a
string, `channelsNamed(name)` gives the channels it means, by its id or
its `serverId:channelId` form, and `nameShown(ref)` the name an agent is
shown for a channel.
- **Unchanged:** `startTyping`, `stopTyping` and `publishForAgent` still
accept a bare id, as connectome-host's activity and notices modules pass
one, and also take a ref or a `serverId:channelId` name. Where two
servers register the id, what a bare id does changes, as Fixed says:
typing shows in each server's channel, and `publishForAgent` refuses it.
`InferenceRequest.channelId` stays. No tool's description or schema
changes.
41 changes: 41 additions & 0 deletions changelog.d/shared-channel-ids.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
- Each MCPL server is told only of its own channels in
`context/beforeInference`'s channel context (SPEC §14.4): `incoming` and
`defaultOutgoing` only when that channel is the server's, and `candidates`
only its own open channels. Every capable server was told the same
context, other servers' channels included, by their ids alone, which a
server registering the same id would read as its own. A server that read
other servers' channels from it no longer hears of them.
- A `[prose-routing]` bounce that lists the channels its target could mean
gives its resend examples with the first of them written as one token,
such as `>>a:general {{unsent}}` and `destination: "a:general"`, where it
gave `>>#channel` and `"#channel"` whatever it listed.
- The `mcpl:conversation-spawned`, `-spawn-failed`, `-unrouted`,
`-binding-orphaned`, `-closed` and `-disposed` traces carry the channel's
`serverId`, and a refusal's failure log names its locus as the agent is
shown it.
- `inference:started` carries the server of its `channelId` as `serverId`,
so a host that posts back to the turn's channel can name it where two
servers register the id. `inference:tokens` and `inference:content_block`
carry the id alone.
- `channel_list` gives a channel whose id names another channel too, another
server's channel with that id or another channel's server-qualified id, a
`name`: its server-qualified id, where that names it alone, which
`channelId` takes, and a `>>` target too where it is one token. Other rows
are as they were.
- The results of `channel_open`, `channel_close` and `channel_decline` carry
the channel's `serverId` in their `data`.
- An open that a tune-out overtook ends as `channel_open`'s refusal of a
tuned-out channel does, saying how the tune-out ends: by cancelling it,
which delivers the diverted backlog, or, for a channel no name picks
alone, by itself.
- A fork's channel refusals quote its channel and what the call asked for.
- A `serverId` given to `channel_publish`, whose schema has none, is read
beside a `channelId` as that exact pair, and with no `channelId` as the
default channel's id on that server, where it was ignored.
- A drafts resend whose destination names a channel that takes no posts is
refused before anything is sent, as a `>>` target naming one is; before,
the send refused its first draft.
- In explicit mode, a start whose wake names no channel, such as a budget
restart or a re-inference, keeps typing in the channel the turn shows,
starting it again where a stop or a failed start ended it, and its token
traces carry that channel's id, where they carried none.
99 changes: 99 additions & 0 deletions changelog.d/shared-channel-ids.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
- Two MCPL servers can register the same channel id (MCPL requires one to be
unique only within a server's connection), and the framework now tells
their channels apart wherever it acts on one, instead of taking whichever
server registered the id first, or refusing the id outright.
- A turn's plain speech, its typing indicator and its outgoing stream go to
the channel on the server the turn's message came from. Speech to a
shared id was refused as ambiguous, with a failure marker, and typing
showed on the server that registered the id first. The `[routing]` and
`[channels]` notices, the turn's receipt, a bounce, the drafts'
destinations and the channel tools' results and refusals name such a
channel by its server-qualified id, `serverId:channelId`.
- `channel_publish` and the subconscious's `speak_in_channel` refuse a
shared id, naming each candidate by its server-qualified id, and take
that form in `channelId` to name one server's channel. Both posted on the
server that registered the id first. `tune_out` does the same, where it
tuned out the channel on the server that registered the id first, and its
result names the channel as the agent can name it back.
`channel_open`, `channel_close` and `channel_decline` take the form too,
alone, and a `serverId` beside a `channelId` names that server's channel
with that exact id. The refusal names each candidate so it can be told
apart, without "not found". Ids hold colons, so two channels'
server-qualified ids can be one string, as `a`'s `b:room` and `a:b`'s
`room` are: such a name is refused as a shared id is, and a channel no
string picks alone is named by its id and server, `"b:room" on server
"a"`. No tool's description or schema changes.
- The subconscious's `cancel_tuneout` reads a name as `tune_out` does: it
refuses a name two registered channels answer to, naming both, and takes
the server-qualified form, where it cancelled the one on the server that
registered the id first. Its `deliver_summary` refuses such a name too,
and tags the summary with the channel's server, where it stored the id
alone. Tune-out's notices, and the reasons its wakes
give, name such a channel by its server-qualified id, and `tune_out`
refuses a channel because a conversation is bound to it only when that
conversation is bound to that server's channel.
- A `>>` or `>>>` target takes the server-qualified id, and a shared id or
label is refused with its candidates named so each can be told apart.
`>>b:general` matched nothing, and a shared label's candidates read as
the same id twice. `prose_help`'s reference shows the form. A `<@id>`
mention or an `@name` that one person's DMs on two servers answer to is
refused with both named apart, where a mention was refused as matching
no DM. A closed-channel invitation for a channel whose id another server
shares offers a `>>` reply naming its server.
- A host's bare id to `startTyping` or `stopTyping` types in each server's
channel with that id, so connectome-host's activity indicator shows in
both, rather than on the first registered server. Where two servers reach
one platform channel, two bots can show typing there.
- `publishForAgent` refuses a shared bare id, where it posted on the first
registered server. A host that passes the server as a ref is answered
there, as one that passes `serverId:channelId` is where that names one
channel alone.
- A push's channel is registered for the server that pushed it, whatever
another server registers, and its reply goes back through that server.
A push leaves the entry of another server's channel with that id as it
was.
- Within a turn, a `>>` or `>>>` target, a drafts resend's destination,
and the `channelId` of `channel_publish`, `channel_open`,
`channel_close`, `channel_decline`, `tune_out` and the subconscious's
`cancel_tuneout`, `speak_in_channel` and `deliver_summary`, that name
the id of a channel the turn has been in (the channel of the message it
answers, and any a mid-turn message or `channel_open` moved it to) mean
that channel on its server, and, where two of the turn's channels on
two servers have that id, neither, with each named. Where another
server registers the same id, such a reply
goes back where the message came from, where it was refused as a shared
id; once that channel's server no longer registers it, the call is
refused, where it went out through, or acted on, another server's
channel with the id. A tool call takes the turn's channels only
when it is made inside the turn, and a script's call only when the turn
that started the script is the one waiting on it, so a script left
running after its turn takes none, and a later turn's channels aren't
lent to it.
- `channel_publish` with no `channelId` posts to its default channel, the
latest message's, on the server that message came from, and is refused
once that server no longer registers the channel, where it went out
through another server that registers the id. The refusal names no other
channel to post to, since the one with that id that remains is another
server's.
- A wake that names its channel without the server, such as a host's
inference request, by an id two servers share or that no registered
channel has, gives the turn no channel and no fallback. Its speech isn't
published, as before, and the agent is no longer told its speech lands in
the first registered server's channel.
- The deprecated conversation routing gives each server's channel its own
fork, where a second server's channel with the same id reached the first
one's fork. A fork's `channel_publish` and `channel_close` of its home's
id act on its home, on its server, where they acted on the first
registered server's channel, and a home its server no longer registers is
refused as such. An addressed wake's route applies to the fork homed on
that server's channel only.
- Coalesced work for a channel is admitted only while its own server
registers the channel, not another server's channel with the same id.
- A batched gate wake's summary lists two servers' channels with one id
apart, each with its count, where it merged them into one line under
the first one's label.
- Two servers' wakes on one channel id at one moment are two wakes to a
provider hold, where one was dropped as the other's duplicate.
- The registry keeps each channel under its server and id together, so
server `a`'s channel `b:room` and server `a:b`'s channel `room`, which a
key joined with a colon gave one entry, are two.
20 changes: 19 additions & 1 deletion docs/explicit-prose-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,28 @@ A prose segment's FIRST line may carry a routing prefix:
```
>>#channel-name send to the channel with this label
>>@person send to the DM with this person
>>discord:guild:id send by exact channel id (always unambiguous)
>>discord:guild:id send by exact channel id
>>srv:discord:guild:id the same channel id on the MCPL server `srv`, where more than one server registers it
>>skip_reply do not send; text stays in context only (mirrors the skip_reply tool)
```

A channel id is unique only within one MCPL server's connection, so two
servers can register the same one. A target that more than one channel
answers to is refused, with its candidates named so each can be told apart:
by its id, or, where its id is shared, in the server-qualified form, or, where
that is shared too (ids hold colons), by its id and server, which no target
takes. The server-qualified form is looked up among the registered pairs,
never split at a colon. Within a turn, an id that one of the turn's own
channels has, a channel the turn has been in (the channel of the message it
answers, and any a mid-turn message or channel_open moved it to), means that
channel, on its server, even where another server registers the id, and is
refused once that server no longer registers it: the words go only where the
message came from, never through another server that has the id. Where two of
the turn's channels have that id, on two servers, it means neither, and is
refused with each named. The channel tools, tune_out and the subconscious's
cancel_tuneout, speak_in_channel and deliver_summary read a name the turn gives
them the same way.

- Body may start on the same line (`>>#ops deploy done`) or the next.
- **Sticky within the turn:** the latest resolved target applies to later
unprefixed segments of the SAME turn, including those a later stream of it
Expand Down
3 changes: 3 additions & 0 deletions src/code-execution/isolate/scripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export interface ScriptCallContext {
watch?: ScriptWatch;
/** The call watching the script now, if one is: a result that ends the turn ends it at that call's return. Without it, `watch`. */
watching?(): ScriptWatch | undefined;
/** The turn the script's run call was made in, when one was: the turn its words were written in, which a later turn's wait doesn't change. */
startTurnId?: number;
}

export interface ScriptsHost {
Expand Down Expand Up @@ -231,6 +233,7 @@ export class AgentScripts {
...script.context,
...(script.watch ? { watch: script.watch } : {}),
watching: () => script.watch ?? undefined,
...(script.turnId !== undefined ? { startTurnId: script.turnId } : {}),
}),
onWake: (payload, n) => this.wake(script, payload, n),
onEnd: (ending) => this.ended(script, ending),
Expand Down
Loading
Loading