@animalabs/agent-framework 0.22.5 → 0.22.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/framework.d.ts +15 -1
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +194 -56
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +3 -0
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +3 -1
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +42 -5
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +96 -27
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +32 -7
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +52 -15
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts +19 -3
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +48 -3
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/types/agent.d.ts +6 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/framework.ts +222 -48
- package/src/gate/event-gate.ts +6 -1
- package/src/mcpl/channel-registry.ts +110 -25
- package/src/mcpl/conversation-router.ts +65 -14
- package/src/mcpl/push-handler.ts +52 -2
- package/src/types/agent.ts +6 -0
package/src/framework.ts
CHANGED
|
@@ -754,6 +754,31 @@ interface TurnRouting {
|
|
|
754
754
|
* sent Sol's DM reply to a stale guild channel (2026-07-22).
|
|
755
755
|
*/
|
|
756
756
|
triggerChannel?: string;
|
|
757
|
+
/**
|
|
758
|
+
* The trigger's channel with the server whose message it was, frozen when
|
|
759
|
+
* the turn opens and never moved: a channel_open moves triggerChannel and
|
|
760
|
+
* a re-pin moves the locus, but neither carries this server with it. Every
|
|
761
|
+
* send of the turn whose destination id is this channel resolves with this
|
|
762
|
+
* server (triggerServerFor): speech, a channel_publish naming it or taking
|
|
763
|
+
* it as its default, a drafts resend, and the refusal reaction. So a
|
|
764
|
+
* message one server sent can't have its reply go out through another
|
|
765
|
+
* server that registers the same id, whatever the registry holds by then;
|
|
766
|
+
* the reply is refused where its server no longer registers the channel.
|
|
767
|
+
* Unset for a turn with no channel trigger, and where the requests batched
|
|
768
|
+
* into the trigger name its channel from more than one server: a bare id
|
|
769
|
+
* then resolves only where exactly one server registers it.
|
|
770
|
+
*/
|
|
771
|
+
triggerOrigin?: { channelId: string; serverId: string };
|
|
772
|
+
/**
|
|
773
|
+
* The locus with the server of the message that put it there, where that
|
|
774
|
+
* isn't the trigger: the latest accepted message's (the global fallback a
|
|
775
|
+
* turn with no trigger channel takes), an arrival's or a mid-turn
|
|
776
|
+
* message's (a re-pin), or the server a channel_open opened it on. Set
|
|
777
|
+
* with the locus (setTurnLocus), and cleared when the locus moves to a
|
|
778
|
+
* channel no one server put there. A send into this channel resolves with
|
|
779
|
+
* this server, as one into the trigger's channel does with the trigger's.
|
|
780
|
+
*/
|
|
781
|
+
locusOrigin?: { channelId: string; serverId: string };
|
|
757
782
|
/**
|
|
758
783
|
* Locus and hybrid mode: where the turn's unpinned plain prose lands.
|
|
759
784
|
* Frozen when the turn opens (home channel for a fork, else the trigger
|
|
@@ -830,11 +855,60 @@ interface Arrival {
|
|
|
830
855
|
messageId: MessageId;
|
|
831
856
|
/** The channel it re-pins the locus to. */
|
|
832
857
|
channelId: string;
|
|
858
|
+
/** The server whose message it is. */
|
|
859
|
+
serverId?: string;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/** The server a stored channel message came from, as the host wrote it. */
|
|
863
|
+
function messageServerId(metadata: MessageMetadata | undefined): string | undefined {
|
|
864
|
+
const serverId = (metadata as { serverId?: unknown } | undefined)?.serverId;
|
|
865
|
+
return typeof serverId === 'string' && serverId ? serverId : undefined;
|
|
833
866
|
}
|
|
834
867
|
|
|
835
868
|
/** The last TurnRouting.turnId given out: ids increase, and are unique within the process. */
|
|
836
869
|
let lastTurnId = 0;
|
|
837
870
|
|
|
871
|
+
/**
|
|
872
|
+
* The server every request naming `channelId` came from, or undefined where
|
|
873
|
+
* one names no server or they name more than one (TurnRouting.triggerOrigin).
|
|
874
|
+
*/
|
|
875
|
+
function onlyServerFor(requests: readonly InferenceRequest[], channelId: string): string | undefined {
|
|
876
|
+
let serverId: string | undefined;
|
|
877
|
+
for (const r of requests) {
|
|
878
|
+
if (r.channelId !== channelId) continue;
|
|
879
|
+
if (!r.serverId || (serverId !== undefined && r.serverId !== serverId)) return undefined;
|
|
880
|
+
serverId = r.serverId;
|
|
881
|
+
}
|
|
882
|
+
return serverId;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* The server a send in `routing`'s turn resolves `channelId` with: the
|
|
887
|
+
* trigger's, where `channelId` is the trigger's channel
|
|
888
|
+
* (TurnRouting.triggerOrigin), else the locus's, where it's the channel the
|
|
889
|
+
* locus's server put there (TurnRouting.locusOrigin).
|
|
890
|
+
*/
|
|
891
|
+
function triggerServerFor(routing: TurnRouting | undefined, channelId: string | null | undefined): string | undefined {
|
|
892
|
+
if (routing === undefined || !channelId) return undefined;
|
|
893
|
+
if (routing.triggerOrigin?.channelId === channelId) return routing.triggerOrigin.serverId;
|
|
894
|
+
if (routing.locusOrigin?.channelId === channelId) return routing.locusOrigin.serverId;
|
|
895
|
+
return undefined;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/** Move a turn's locus, with the server of the message that put it there
|
|
899
|
+
* (TurnRouting.locusOrigin), or none. */
|
|
900
|
+
function setTurnLocus(routing: TurnRouting, channelId: string, serverId: string | undefined): void {
|
|
901
|
+
routing.locus = channelId;
|
|
902
|
+
routing.locusOrigin = serverId !== undefined ? { channelId, serverId } : undefined;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/** routeSpeech's options for speech into `channelId` in `routing`'s turn:
|
|
906
|
+
* the trigger's server, where it's the trigger's channel (triggerServerFor). */
|
|
907
|
+
function speechServerOpts(routing: TurnRouting | undefined, channelId: string | null | undefined): { locusServerId: string } | undefined {
|
|
908
|
+
const serverId = triggerServerFor(routing, channelId);
|
|
909
|
+
return serverId !== undefined ? { locusServerId: serverId } : undefined;
|
|
910
|
+
}
|
|
911
|
+
|
|
838
912
|
function freshTurnRouting(): TurnRouting {
|
|
839
913
|
return {
|
|
840
914
|
turnId: ++lastTurnId,
|
|
@@ -2727,8 +2801,8 @@ export class AgentFramework {
|
|
|
2727
2801
|
// route applies to a fork only when it IS that home, and never to
|
|
2728
2802
|
// another agent when a fork owns that channel. Otherwise a fork
|
|
2729
2803
|
// bound elsewhere would show typing where it isn't replying.
|
|
2730
|
-
...(provenance?.routeChannelId && framework.mayTakeGateRoute(agentName, provenance.routeChannelId)
|
|
2731
|
-
? { channelId: provenance.routeChannelId, addressed: true }
|
|
2804
|
+
...(provenance?.routeChannelId && framework.mayTakeGateRoute(agentName, provenance.routeChannelId, provenance.routeServerId)
|
|
2805
|
+
? { channelId: provenance.routeChannelId, serverId: provenance.routeServerId, addressed: true }
|
|
2732
2806
|
: {}),
|
|
2733
2807
|
});
|
|
2734
2808
|
},
|
|
@@ -7960,7 +8034,9 @@ export class AgentFramework {
|
|
|
7960
8034
|
// tools/call directly on its connection (bare tool name — no prefix
|
|
7961
8035
|
// games), bypassing the agent event queue so no synthetic tool-result
|
|
7962
8036
|
// is injected into a turn the agent never took.
|
|
7963
|
-
|
|
8037
|
+
// Through the server whose message it marks: the latest accepted
|
|
8038
|
+
// message's, kept beside its channel and id (fallbackServerFor).
|
|
8039
|
+
const serverId = this.channelRegistry?.fallbackServerFor(incoming.channelId);
|
|
7964
8040
|
const server = serverId ? this.mcplServerRegistry?.getServer(serverId) : null;
|
|
7965
8041
|
if (!server) {
|
|
7966
8042
|
console.error(
|
|
@@ -8133,7 +8209,9 @@ export class AgentFramework {
|
|
|
8133
8209
|
const parts = incoming.channelId.split(':');
|
|
8134
8210
|
if (parts[0] !== 'discord') return;
|
|
8135
8211
|
const channelId = parts[parts.length - 1];
|
|
8136
|
-
|
|
8212
|
+
// As reactToRefusal resolves it: through the server whose message the
|
|
8213
|
+
// latest inbound is (fallbackServerFor).
|
|
8214
|
+
const serverId = this.channelRegistry?.fallbackServerFor(incoming.channelId);
|
|
8137
8215
|
const server = serverId ? this.mcplServerRegistry?.getServer(serverId) : null;
|
|
8138
8216
|
if (!server) return;
|
|
8139
8217
|
await server.sendToolsCall('send_message', {
|
|
@@ -10529,12 +10607,19 @@ export class AgentFramework {
|
|
|
10529
10607
|
// The last injection that qualifies wins (repinChannel).
|
|
10530
10608
|
const routing = this.routingOf(agent.name);
|
|
10531
10609
|
let newLocus: string | undefined;
|
|
10532
|
-
|
|
10610
|
+
let newLocusServer: string | undefined;
|
|
10611
|
+
for (const inj of midTurnInjections) {
|
|
10612
|
+
const channelId = this.repinChannel(inj.metadata, routing);
|
|
10613
|
+
if (channelId) {
|
|
10614
|
+
newLocus = channelId;
|
|
10615
|
+
newLocusServer = messageServerId(inj.metadata);
|
|
10616
|
+
}
|
|
10617
|
+
}
|
|
10533
10618
|
if (newLocus) {
|
|
10534
10619
|
// What this boundary just stored is newer than any arrival the
|
|
10535
10620
|
// turn holds, so the arrivals can't move the locus back.
|
|
10536
10621
|
routing.arrivals = [];
|
|
10537
|
-
const notice = this.repinTurnLocus(agent, routing, newLocus, 'mid-turn addressed');
|
|
10622
|
+
const notice = this.repinTurnLocus(agent, routing, newLocus, 'mid-turn addressed', newLocusServer);
|
|
10538
10623
|
if (notice) midTurnInjections.push(notice);
|
|
10539
10624
|
}
|
|
10540
10625
|
} else if (routesLocus && !shouldEndTurn) {
|
|
@@ -10917,14 +11002,15 @@ export class AgentFramework {
|
|
|
10917
11002
|
const id = target.getContextManager().addMessage('user', event.content, metadata);
|
|
10918
11003
|
// A correction may target an older engagement. Refresh only the
|
|
10919
11004
|
// binding whose agent actually received the message.
|
|
10920
|
-
|
|
10921
|
-
|
|
11005
|
+
const channel = { serverId: event.serverId, channelId: event.channelId };
|
|
11006
|
+
if (this.conversationRouter.getBinding(channel)?.agentName === target.name) {
|
|
11007
|
+
this.conversationRouter.touch(channel);
|
|
10922
11008
|
}
|
|
10923
11009
|
this.emitTrace({ type: 'message:added', messageId: id, source: 'mcpl:channel-incoming' });
|
|
10924
11010
|
if (event.triggerInference) {
|
|
10925
11011
|
this.pendingRequests.push({
|
|
10926
11012
|
agentName: target.name, reason: 'mcpl:channel-incoming', source: event.serverId, timestamp: Date.now(),
|
|
10927
|
-
channelId: event.channelId,
|
|
11013
|
+
channelId: event.channelId, serverId: event.serverId,
|
|
10928
11014
|
counterparty: event.author?.id ? `${event.serverId}:user:${event.author.id}` : undefined,
|
|
10929
11015
|
addressed: isAddressedMessage(event.tags, event.metadata),
|
|
10930
11016
|
...(event.coalescingSubject ? { coalescingSubject: event.coalescingSubject } : {}),
|
|
@@ -11003,8 +11089,10 @@ export class AgentFramework {
|
|
|
11003
11089
|
source: event.serverId,
|
|
11004
11090
|
timestamp: Date.now(),
|
|
11005
11091
|
// Route this turn's auto-published speech back to THIS channel, not
|
|
11006
|
-
// the global most-recent-inbound locus (item-3 redux, trunk agents)
|
|
11092
|
+
// the global most-recent-inbound locus (item-3 redux, trunk agents),
|
|
11093
|
+
// through the server that sent it.
|
|
11007
11094
|
channelId: event.channelId,
|
|
11095
|
+
serverId: event.serverId,
|
|
11008
11096
|
counterparty: event.author?.id ? `${event.serverId}:user:${event.author.id}` : undefined,
|
|
11009
11097
|
// Addressed messages outrank ambient chatter when a batched wake
|
|
11010
11098
|
// picks the turn's frozen speech locus.
|
|
@@ -11082,7 +11170,7 @@ export class AgentFramework {
|
|
|
11082
11170
|
if (!agent) {
|
|
11083
11171
|
// Bound agent vanished (e.g. external reset) — unbind so the next
|
|
11084
11172
|
// qualifying message respawns a fresh fork.
|
|
11085
|
-
router.unbind(event.channelId);
|
|
11173
|
+
router.unbind({ serverId: event.serverId, channelId: event.channelId });
|
|
11086
11174
|
this.emitTrace({
|
|
11087
11175
|
type: 'mcpl:conversation-binding-orphaned',
|
|
11088
11176
|
channelId: event.channelId,
|
|
@@ -11098,7 +11186,7 @@ export class AgentFramework {
|
|
|
11098
11186
|
messageMetadata.triggered = trigger;
|
|
11099
11187
|
|
|
11100
11188
|
const id = agent.getContextManager().addMessage('user', event.content, messageMetadata);
|
|
11101
|
-
router.touch(event.channelId);
|
|
11189
|
+
router.touch({ serverId: event.serverId, channelId: event.channelId });
|
|
11102
11190
|
this.emitTrace({ type: 'message:added', messageId: id, source: 'mcpl:channel-incoming' });
|
|
11103
11191
|
|
|
11104
11192
|
if (trigger) {
|
|
@@ -11110,6 +11198,7 @@ export class AgentFramework {
|
|
|
11110
11198
|
// A fork's home channel wins in routeSpeech regardless, but carry the
|
|
11111
11199
|
// triggering channel too so the trunk/active path stays consistent.
|
|
11112
11200
|
channelId: event.channelId,
|
|
11201
|
+
serverId: event.serverId,
|
|
11113
11202
|
counterparty: event.author?.id ? `${event.serverId}:user:${event.author.id}` : undefined,
|
|
11114
11203
|
addressed: isAddressedMessage(event.tags, event.metadata),
|
|
11115
11204
|
...(event.coalescingSubject ? { coalescingSubject: event.coalescingSubject } : {}),
|
|
@@ -11254,7 +11343,7 @@ export class AgentFramework {
|
|
|
11254
11343
|
}
|
|
11255
11344
|
const decision = this.conversationRouter!.route(facts);
|
|
11256
11345
|
if (decision.kind !== 'spawn') return decision;
|
|
11257
|
-
return { ...decision, spawned: this.spawnConversationAgent(facts
|
|
11346
|
+
return { ...decision, spawned: this.spawnConversationAgent(facts, decision) };
|
|
11258
11347
|
}
|
|
11259
11348
|
}
|
|
11260
11349
|
|
|
@@ -11270,11 +11359,12 @@ export class AgentFramework {
|
|
|
11270
11359
|
* so the entry it removes is its own. It's removed before anyone waiting
|
|
11271
11360
|
* on it resumes, so a waiter that decides again doesn't find it.
|
|
11272
11361
|
*/
|
|
11273
|
-
private spawnConversationAgent(
|
|
11362
|
+
private spawnConversationAgent(facts: IncomingMessageFacts, decision: { agentName: string; generation: number }): Promise<Agent> {
|
|
11363
|
+
const channelId = facts.channelId;
|
|
11274
11364
|
const spawn = (async () => {
|
|
11275
11365
|
this.storeConversationGeneration(channelId, decision.generation);
|
|
11276
11366
|
const agent = await this.createConversationAgent(decision.agentName, channelId);
|
|
11277
|
-
this.conversationRouter!.bind(channelId, decision.agentName, decision.generation);
|
|
11367
|
+
this.conversationRouter!.bind({ serverId: facts.serverId, channelId }, decision.agentName, decision.generation);
|
|
11278
11368
|
return agent;
|
|
11279
11369
|
})();
|
|
11280
11370
|
const done = (): void => { this.conversationSpawns.delete(channelId); };
|
|
@@ -11403,7 +11493,7 @@ export class AgentFramework {
|
|
|
11403
11493
|
this.lastConversationSweep = now;
|
|
11404
11494
|
|
|
11405
11495
|
for (const binding of this.conversationRouter.expired(now)) {
|
|
11406
|
-
this.conversationRouter.unbind(binding
|
|
11496
|
+
this.conversationRouter.unbind(binding);
|
|
11407
11497
|
this.emitTrace({
|
|
11408
11498
|
type: 'mcpl:conversation-closed',
|
|
11409
11499
|
channelId: binding.channelId,
|
|
@@ -12072,7 +12162,7 @@ export class AgentFramework {
|
|
|
12072
12162
|
if (explicit) return explicit.filter((n) => this.agents.has(n));
|
|
12073
12163
|
if (occ.scope.kind === 'channel' && this.conversationRouter) {
|
|
12074
12164
|
const router = this.conversationRouter;
|
|
12075
|
-
const bound = router.getBinding(occ.scope.id)?.agentName;
|
|
12165
|
+
const bound = router.getBinding({ serverId: occ.serverId, channelId: occ.scope.id })?.agentName;
|
|
12076
12166
|
if (bound) return this.agents.has(bound) ? [bound] : [];
|
|
12077
12167
|
// No fork yet: the same bind decision an incoming message would get. A
|
|
12078
12168
|
// batch that qualifies spawns its fork now so it has someone to wake.
|
|
@@ -12180,6 +12270,7 @@ export class AgentFramework {
|
|
|
12180
12270
|
source: occ.serverId,
|
|
12181
12271
|
timestamp: Date.now(),
|
|
12182
12272
|
channelId,
|
|
12273
|
+
serverId: channelId !== undefined ? occ.serverId : undefined,
|
|
12183
12274
|
counterparty: authorId ? `${occ.serverId}:user:${authorId}` : undefined,
|
|
12184
12275
|
addressed: isAddressedMessage(occ.tags, origin),
|
|
12185
12276
|
coalescingSubject: subject,
|
|
@@ -12394,6 +12485,7 @@ export class AgentFramework {
|
|
|
12394
12485
|
source: event.serverId,
|
|
12395
12486
|
timestamp: Date.now(),
|
|
12396
12487
|
channelId: triggerChannel?.channelId,
|
|
12488
|
+
serverId: triggerChannel ? event.serverId : undefined,
|
|
12397
12489
|
// DMs and addressed-while-closed messages arrive as push events;
|
|
12398
12490
|
// they must outrank ambient chatter in a batched wake's locus
|
|
12399
12491
|
// selection just like their channels/incoming counterparts.
|
|
@@ -12416,17 +12508,49 @@ export class AgentFramework {
|
|
|
12416
12508
|
* Whether a batched gate wake's addressed route may become `agentName`'s
|
|
12417
12509
|
* speech locus. The gate broadcasts to every agent, conversation forks
|
|
12418
12510
|
* included. A fork speaks only in its home channel, and a channel bound to
|
|
12419
|
-
* a fork is that fork's conversation, so no other agent takes it.
|
|
12511
|
+
* a fork is that fork's conversation, so no other agent takes it. Two
|
|
12512
|
+
* servers' channels with one id are two conversations, each bound to its
|
|
12513
|
+
* own fork with that id as its home, so a fork takes the route only where
|
|
12514
|
+
* the router binds the route's channel, its server and id, to it; a route
|
|
12515
|
+
* with no server names a fork only where one fork is bound to that id.
|
|
12420
12516
|
*/
|
|
12421
|
-
private mayTakeGateRoute(agentName: string, routeChannelId: string): boolean {
|
|
12517
|
+
private mayTakeGateRoute(agentName: string, routeChannelId: string, routeServerId?: string): boolean {
|
|
12422
12518
|
const home = this.conversationAgentHomes.get(agentName);
|
|
12423
|
-
if (home !== undefined)
|
|
12519
|
+
if (home !== undefined) {
|
|
12520
|
+
return home === routeChannelId
|
|
12521
|
+
&& this.conversationRouter?.getBinding({ serverId: routeServerId, channelId: routeChannelId })?.agentName === agentName;
|
|
12522
|
+
}
|
|
12424
12523
|
for (const boundChannel of this.conversationAgentHomes.values()) {
|
|
12425
12524
|
if (boundChannel === routeChannelId) return false;
|
|
12426
12525
|
}
|
|
12427
12526
|
return true;
|
|
12428
12527
|
}
|
|
12429
12528
|
|
|
12529
|
+
/**
|
|
12530
|
+
* The channel a push names, and a server that registers it, when another
|
|
12531
|
+
* server registers that channel id and the pushing server doesn't. A
|
|
12532
|
+
* channel's id is the agent's locus on its own, so the reply to such a
|
|
12533
|
+
* push would go out through the server that registers it: the push
|
|
12534
|
+
* boundary refuses it (PushHandler). A push naming a channel no server
|
|
12535
|
+
* registers isn't refused, and registers it for the pushing server
|
|
12536
|
+
* (ensureChannelRegistered).
|
|
12537
|
+
*/
|
|
12538
|
+
private channelOfAnotherServer(
|
|
12539
|
+
serverId: string,
|
|
12540
|
+
origin: Record<string, unknown> | undefined,
|
|
12541
|
+
): { channelId: string; holder: string } | undefined {
|
|
12542
|
+
if (!this.channelRegistry) return undefined;
|
|
12543
|
+
const channel = this.derivePushEventChannel(origin);
|
|
12544
|
+
if (!channel) return undefined;
|
|
12545
|
+
let holder: string | undefined;
|
|
12546
|
+
for (const entry of this.channelRegistry.listChannelsRaw()) {
|
|
12547
|
+
if (entry.descriptor.id !== channel.channelId) continue;
|
|
12548
|
+
if (entry.serverId === serverId) return undefined;
|
|
12549
|
+
holder ??= entry.serverId;
|
|
12550
|
+
}
|
|
12551
|
+
return holder === undefined ? undefined : { channelId: channel.channelId, holder };
|
|
12552
|
+
}
|
|
12553
|
+
|
|
12430
12554
|
/**
|
|
12431
12555
|
* Derive the MCPL composite channel id (the outbound routing locus) for a
|
|
12432
12556
|
* push event from its server-defined `origin`, if it carries one.
|
|
@@ -12751,6 +12875,7 @@ export class AgentFramework {
|
|
|
12751
12875
|
ephemeralSystemPrompt: silentOnly ? trigger?.ephemeralSystemPrompt : undefined,
|
|
12752
12876
|
silentHeartbeat: silentOnly ? trigger?.silentHeartbeat : undefined,
|
|
12753
12877
|
channelId: channelReq?.channelId,
|
|
12878
|
+
serverId: channelReq?.channelId !== undefined ? onlyServerFor(requests, channelReq.channelId) : undefined,
|
|
12754
12879
|
addressed: addressedReq !== undefined,
|
|
12755
12880
|
// A context-budget restart continues the same logical turn: it keeps
|
|
12756
12881
|
// the channel for routing but names no author — the restart is its
|
|
@@ -13197,8 +13322,8 @@ export class AgentFramework {
|
|
|
13197
13322
|
/** The drafts tool's answer to a resident's call, from the model's own
|
|
13198
13323
|
* dispatch or an agent-origin call through executeToolCall (a puppet's):
|
|
13199
13324
|
* a failure the handler throws is answered as one. */
|
|
13200
|
-
private draftsToolResult(agentName: string, rawInput: unknown): Promise<ToolResult> {
|
|
13201
|
-
return this.handleDraftsTool(agentName, rawInput).catch((err): ToolResult => ({
|
|
13325
|
+
private draftsToolResult(agentName: string, rawInput: unknown, callerTurnId?: number): Promise<ToolResult> {
|
|
13326
|
+
return this.handleDraftsTool(agentName, rawInput, callerTurnId).catch((err): ToolResult => ({
|
|
13202
13327
|
success: false,
|
|
13203
13328
|
error: `drafts failed: ${thrownText(err)}`,
|
|
13204
13329
|
isError: true,
|
|
@@ -13211,7 +13336,7 @@ export class AgentFramework {
|
|
|
13211
13336
|
* each draft is published verbatim, in order, through the normal publish
|
|
13212
13337
|
* executor, its attempt journaled durably before the request leaves.
|
|
13213
13338
|
*/
|
|
13214
|
-
private async handleDraftsTool(agentName: string, rawInput: unknown): Promise<ToolResult> {
|
|
13339
|
+
private async handleDraftsTool(agentName: string, rawInput: unknown, callerTurnId?: number): Promise<ToolResult> {
|
|
13215
13340
|
const input = (rawInput && typeof rawInput === 'object' && !Array.isArray(rawInput) ? rawInput : {}) as Record<string, unknown>;
|
|
13216
13341
|
// The turn that sends: a resent draft's channel is recorded as engaged on
|
|
13217
13342
|
// it, as a send tool's is, even when the result comes after the turn.
|
|
@@ -13378,7 +13503,17 @@ export class AgentFramework {
|
|
|
13378
13503
|
const cand = resolved.candidates?.length ? ` It could mean: ${resolved.candidates.join(', ')}.` : '';
|
|
13379
13504
|
return refuse(`Destination "${spec}" did not resolve: ${resolved.error}.${cand} Nothing was sent.`);
|
|
13380
13505
|
}
|
|
13381
|
-
|
|
13506
|
+
// A resend into a channel the calling turn recorded a server for goes
|
|
13507
|
+
// through that server (triggerServerFor), where the call was made inside
|
|
13508
|
+
// that turn; a background script's call carries no turn.
|
|
13509
|
+
const callingRouting = this.turnRouting.get(agentName);
|
|
13510
|
+
const triggerServer = callerTurnId !== undefined && callingRouting?.turnId === callerTurnId
|
|
13511
|
+
? triggerServerFor(callingRouting, resolved.channelId)
|
|
13512
|
+
: undefined;
|
|
13513
|
+
const target = registry.resolveDestination({
|
|
13514
|
+
...(triggerServer !== undefined ? { serverId: triggerServer } : {}),
|
|
13515
|
+
channelId: resolved.channelId,
|
|
13516
|
+
});
|
|
13382
13517
|
if ('error' in target) return refuse(`Destination "${spec}": ${target.error}. Nothing was sent.`);
|
|
13383
13518
|
const destination = target.destination;
|
|
13384
13519
|
if (home && destination.channelId !== home) {
|
|
@@ -13724,7 +13859,10 @@ export class AgentFramework {
|
|
|
13724
13859
|
if (!open) return;
|
|
13725
13860
|
if ((metadata as { tuneOut?: unknown } | undefined)?.tuneOut) return;
|
|
13726
13861
|
const channelId = this.repinChannel(metadata, open);
|
|
13727
|
-
if (channelId)
|
|
13862
|
+
if (channelId) {
|
|
13863
|
+
const serverId = messageServerId(metadata);
|
|
13864
|
+
open.arrivals.push({ messageId, channelId, ...(serverId !== undefined ? { serverId } : {}) });
|
|
13865
|
+
}
|
|
13728
13866
|
}
|
|
13729
13867
|
|
|
13730
13868
|
/**
|
|
@@ -13760,14 +13898,14 @@ export class AgentFramework {
|
|
|
13760
13898
|
* message in the store; the last compile can't be the test, since an
|
|
13761
13899
|
* arrival between the turn's streams isn't in it yet.
|
|
13762
13900
|
*/
|
|
13763
|
-
private remainingArrival(agent: Agent, routing: TurnRouting):
|
|
13901
|
+
private remainingArrival(agent: Agent, routing: TurnRouting): Arrival | undefined {
|
|
13764
13902
|
const cm = agent.getContextManager();
|
|
13765
13903
|
for (let i = routing.arrivals.length - 1; i >= 0; i--) {
|
|
13766
|
-
const
|
|
13904
|
+
const arrival = routing.arrivals[i]!;
|
|
13767
13905
|
// The store answers null for a message that isn't there. A store that
|
|
13768
13906
|
// can't be read throws here, as anywhere else in the start, rather
|
|
13769
13907
|
// than have its candidates read as withdrawn.
|
|
13770
|
-
if (cm.getMessage(messageId)) return
|
|
13908
|
+
if (cm.getMessage(arrival.messageId)) return arrival;
|
|
13771
13909
|
}
|
|
13772
13910
|
return undefined;
|
|
13773
13911
|
}
|
|
@@ -13789,10 +13927,12 @@ export class AgentFramework {
|
|
|
13789
13927
|
routing: TurnRouting,
|
|
13790
13928
|
channelId: string,
|
|
13791
13929
|
cause: string,
|
|
13930
|
+
/** The server of the message that moved it. */
|
|
13931
|
+
serverId?: string,
|
|
13792
13932
|
): { participant: string; content: ContentBlock[]; metadata: MessageMetadata } | undefined {
|
|
13793
13933
|
if (routing.locus === channelId) return undefined;
|
|
13794
13934
|
const prevPin = routing.locus ?? null;
|
|
13795
|
-
routing
|
|
13935
|
+
setTurnLocus(routing, channelId, serverId);
|
|
13796
13936
|
const label = this.channelRegistry?.getDescriptor(channelId)?.label;
|
|
13797
13937
|
const shown = label && label !== channelId
|
|
13798
13938
|
? `${label.startsWith('#') ? label : `#${label}`} (${channelId})`
|
|
@@ -14054,11 +14194,13 @@ export class AgentFramework {
|
|
|
14054
14194
|
body: string,
|
|
14055
14195
|
channelId: string,
|
|
14056
14196
|
unsent: Draft | undefined,
|
|
14197
|
+
/** The server the channel resolves with (triggerServerFor). */
|
|
14198
|
+
serverId: string | undefined,
|
|
14057
14199
|
): Promise<PublishOutcome> {
|
|
14058
14200
|
const registry = this.channelRegistry!;
|
|
14059
14201
|
let attemptId: string | undefined;
|
|
14060
14202
|
if (unsent) {
|
|
14061
|
-
const target = registry.resolveDestination({ channelId });
|
|
14203
|
+
const target = registry.resolveDestination({ ...(serverId !== undefined ? { serverId } : {}), channelId });
|
|
14062
14204
|
if (!('error' in target)) {
|
|
14063
14205
|
try {
|
|
14064
14206
|
attemptId = this.proseDrafts.beginAttempt(agent.name, unsent.id, target.destination, 'unsent-token', false);
|
|
@@ -14079,7 +14221,8 @@ export class AgentFramework {
|
|
|
14079
14221
|
try {
|
|
14080
14222
|
// Both callers hold a failed delivery's words as a bounce draft, whose
|
|
14081
14223
|
// notice says what happened, so the registry writes no second marker.
|
|
14082
|
-
outcome = await registry.deliverSpeech(agent.name, body, channelId,
|
|
14224
|
+
outcome = await registry.deliverSpeech(agent.name, body, channelId,
|
|
14225
|
+
{ heldOnFailure: true, ...(serverId !== undefined ? { locusServerId: serverId } : {}) });
|
|
14083
14226
|
} finally {
|
|
14084
14227
|
if (flight) {
|
|
14085
14228
|
this.draftsInFlight.delete(flight);
|
|
@@ -14177,7 +14320,7 @@ export class AgentFramework {
|
|
|
14177
14320
|
continue;
|
|
14178
14321
|
}
|
|
14179
14322
|
try {
|
|
14180
|
-
const outcome = await this.deliverWithUnsent(agent, body, resolved.channelId, unsent.draft);
|
|
14323
|
+
const outcome = await this.deliverWithUnsent(agent, body, resolved.channelId, unsent.draft, triggerServerFor(routing, resolved.channelId));
|
|
14181
14324
|
this.recordProseDelivery(routing, outcome.status === 'delivered' ? { delivered: true, channelId: outcome.destination!.channelId } : null);
|
|
14182
14325
|
if (outcome.status === 'delivered') {
|
|
14183
14326
|
this.proseBounceStreaks.delete(agent.name);
|
|
@@ -14219,7 +14362,7 @@ export class AgentFramework {
|
|
|
14219
14362
|
}
|
|
14220
14363
|
const target = sticky ?? locus;
|
|
14221
14364
|
try {
|
|
14222
|
-
const outcome = await this.channelRegistry!.routeSpeech(agent.name, envelope, target);
|
|
14365
|
+
const outcome = await this.channelRegistry!.routeSpeech(agent.name, envelope, target, speechServerOpts(routing, target));
|
|
14223
14366
|
this.recordProseDelivery(routing, outcome);
|
|
14224
14367
|
} catch (err) {
|
|
14225
14368
|
console.error('hybrid locus delivery failed:', err);
|
|
@@ -14340,7 +14483,7 @@ export class AgentFramework {
|
|
|
14340
14483
|
}
|
|
14341
14484
|
|
|
14342
14485
|
try {
|
|
14343
|
-
const outcome = await this.deliverWithUnsent(agent, body, targetChannel, unsent.draft);
|
|
14486
|
+
const outcome = await this.deliverWithUnsent(agent, body, targetChannel, unsent.draft, triggerServerFor(routing, targetChannel));
|
|
14344
14487
|
if (outcome.status === 'delivered') {
|
|
14345
14488
|
this.proseBounceStreaks.delete(name);
|
|
14346
14489
|
} else if (outcome.status === 'failed') {
|
|
@@ -14931,6 +15074,9 @@ export class AgentFramework {
|
|
|
14931
15074
|
// turn, which falls back to the global default rather than inheriting
|
|
14932
15075
|
// a previous turn's channel.
|
|
14933
15076
|
routing.triggerChannel = trigger?.channelId;
|
|
15077
|
+
routing.triggerOrigin = trigger?.channelId && trigger.serverId
|
|
15078
|
+
? { channelId: trigger.channelId, serverId: trigger.serverId }
|
|
15079
|
+
: undefined;
|
|
14934
15080
|
// FREEZE the turn's outbound locus now (turn-frozen routing): the home
|
|
14935
15081
|
// channel for forks, else the trigger channel, else the global default.
|
|
14936
15082
|
// Mid-turn injections must not redirect the agent's plain prose
|
|
@@ -14945,7 +15091,14 @@ export class AgentFramework {
|
|
|
14945
15091
|
// stability).
|
|
14946
15092
|
if (turnProseRouting !== 'explicit' && turnProseRouting !== 'disabled') {
|
|
14947
15093
|
const locus = this.channelRegistry?.resolveLocus(agent.name) ?? null;
|
|
14948
|
-
|
|
15094
|
+
// A locus that is neither the trigger's channel nor the agent's home
|
|
15095
|
+
// is the global fallback: its server is the latest accepted
|
|
15096
|
+
// message's, taken now, with the locus.
|
|
15097
|
+
if (locus !== null) {
|
|
15098
|
+
setTurnLocus(routing, locus, locus !== routing.triggerChannel && !this.conversationAgentHomes.has(agent.name)
|
|
15099
|
+
? this.channelRegistry?.fallbackServerFor(locus)
|
|
15100
|
+
: undefined);
|
|
15101
|
+
}
|
|
14949
15102
|
if (attempt === 0) this.announceLocusIfChanged(agent.name, locus);
|
|
14950
15103
|
}
|
|
14951
15104
|
} else {
|
|
@@ -14958,7 +15111,7 @@ export class AgentFramework {
|
|
|
14958
15111
|
? this.remainingArrival(agent, routing)
|
|
14959
15112
|
: undefined;
|
|
14960
15113
|
routing.arrivals = [];
|
|
14961
|
-
if (arrival) this.repinTurnLocus(agent, routing, arrival, 'arrival');
|
|
15114
|
+
if (arrival) this.repinTurnLocus(agent, routing, arrival.channelId, 'arrival', arrival.serverId);
|
|
14962
15115
|
}
|
|
14963
15116
|
// A silent start, or a disabled agent's, has no locus to show, whatever a
|
|
14964
15117
|
// channel_open in a silent turn left on its object.
|
|
@@ -15438,7 +15591,7 @@ export class AgentFramework {
|
|
|
15438
15591
|
};
|
|
15439
15592
|
const enqueueSpeech = (text: string, locus: string | null): void => {
|
|
15440
15593
|
chainSpeech(async () => {
|
|
15441
|
-
const outcome = await this.channelRegistry!.routeSpeech(agent.name, text, locus);
|
|
15594
|
+
const outcome = await this.channelRegistry!.routeSpeech(agent.name, text, locus, speechServerOpts(streamRouting, locus));
|
|
15442
15595
|
this.recordProseDelivery(streamRouting, outcome);
|
|
15443
15596
|
}, 'mid-turn speech routing failed:');
|
|
15444
15597
|
};
|
|
@@ -15558,6 +15711,7 @@ export class AgentFramework {
|
|
|
15558
15711
|
for (const rd of deltas) {
|
|
15559
15712
|
this.channelRegistry!.sendOutgoingChunk(
|
|
15560
15713
|
rd.channelId, agent.name, outgoingInferenceId, outgoingIndex++, rd.delta,
|
|
15714
|
+
triggerServerFor(streamRouting, rd.channelId),
|
|
15561
15715
|
);
|
|
15562
15716
|
}
|
|
15563
15717
|
};
|
|
@@ -16363,7 +16517,7 @@ export class AgentFramework {
|
|
|
16363
16517
|
);
|
|
16364
16518
|
for (const segment of speechSegments) {
|
|
16365
16519
|
try {
|
|
16366
|
-
const outcome = await this.channelRegistry.routeSpeech(agent.name, segment, locus);
|
|
16520
|
+
const outcome = await this.channelRegistry.routeSpeech(agent.name, segment, locus, speechServerOpts(streamRouting, locus));
|
|
16367
16521
|
this.recordProseDelivery(streamRouting, outcome);
|
|
16368
16522
|
} catch (err) {
|
|
16369
16523
|
console.error('speech routing failed:', err);
|
|
@@ -16459,7 +16613,7 @@ export class AgentFramework {
|
|
|
16459
16613
|
// Deliver sequentially (await each) so the segments land in order.
|
|
16460
16614
|
for (const seg of segments) {
|
|
16461
16615
|
try {
|
|
16462
|
-
const outcome = await this.channelRegistry.routeSpeech(agent.name, seg, locus);
|
|
16616
|
+
const outcome = await this.channelRegistry.routeSpeech(agent.name, seg, locus, speechServerOpts(streamRouting, locus));
|
|
16463
16617
|
this.recordProseDelivery(streamRouting, outcome);
|
|
16464
16618
|
} catch (err) {
|
|
16465
16619
|
console.error('speech routing failed:', err);
|
|
@@ -17103,7 +17257,7 @@ export class AgentFramework {
|
|
|
17103
17257
|
if (frameReachedTerminal && proseStream) {
|
|
17104
17258
|
emitOutgoing(proseStream.finish());
|
|
17105
17259
|
for (const [channelId, text] of proseStream.byChannel()) {
|
|
17106
|
-
this.channelRegistry!.sendOutgoingComplete(channelId, agent.name, outgoingInferenceId, text);
|
|
17260
|
+
this.channelRegistry!.sendOutgoingComplete(channelId, agent.name, outgoingInferenceId, text, triggerServerFor(streamRouting, channelId));
|
|
17107
17261
|
}
|
|
17108
17262
|
}
|
|
17109
17263
|
this.frameworkCancelledStreams.delete(`${agent.name}:${myStreamId}`);
|
|
@@ -17395,7 +17549,7 @@ export class AgentFramework {
|
|
|
17395
17549
|
// agent origin and acts as that resident, as the model's own call does
|
|
17396
17550
|
// through dispatchToolCall. A module holds no drafts.
|
|
17397
17551
|
if (call.name === 'drafts' && this.channelRegistry && origin.kind === 'agent') {
|
|
17398
|
-
return this.draftsToolResult(origin.agentName, call.input);
|
|
17552
|
+
return this.draftsToolResult(origin.agentName, call.input, call.callerTurnId);
|
|
17399
17553
|
}
|
|
17400
17554
|
|
|
17401
17555
|
// The utils meta-tool — programmatic callers (code_execution scripts,
|
|
@@ -18865,7 +19019,7 @@ export class AgentFramework {
|
|
|
18865
19019
|
if (enrichedCall.name === 'drafts' && this.channelRegistry) {
|
|
18866
19020
|
const startedAt = Date.now();
|
|
18867
19021
|
this.emitTrace({ type: 'tool:started', module: 'framework', tool: 'drafts', callId: enrichedCall.id, input: enrichedCall.input });
|
|
18868
|
-
void this.draftsToolResult(agentName, enrichedCall.input)
|
|
19022
|
+
void this.draftsToolResult(agentName, enrichedCall.input, enrichedCall.callerTurnId)
|
|
18869
19023
|
.then((result) => {
|
|
18870
19024
|
this.emitTrace({ type: 'tool:completed', module: 'framework', tool: 'drafts', callId: enrichedCall.id, durationMs: Date.now() - startedAt });
|
|
18871
19025
|
this.pushEvent({ type: 'tool-result', callId: enrichedCall.id, toolName: enrichedCall.name, agentName, moduleName: 'framework', result });
|
|
@@ -20017,6 +20171,7 @@ export class AgentFramework {
|
|
|
20017
20171
|
this.noteInboundAccepted(source);
|
|
20018
20172
|
return source;
|
|
20019
20173
|
},
|
|
20174
|
+
(serverId, params) => this.channelOfAnotherServer(serverId, params.origin),
|
|
20020
20175
|
);
|
|
20021
20176
|
|
|
20022
20177
|
// Server-initiated inference router (Step 6)
|
|
@@ -22475,8 +22630,17 @@ export class AgentFramework {
|
|
|
22475
22630
|
// ordinary wake, starts only once its over-budget boundary has every
|
|
22476
22631
|
// result of the round.
|
|
22477
22632
|
const callingTurn = this.routingOf(agentName);
|
|
22633
|
+
const inTurn = call.callerTurnId !== undefined && call.callerTurnId === callingTurn.turnId;
|
|
22478
22634
|
|
|
22479
|
-
this.channelRegistry!.handleChannelToolCall(call.name, call.input, {
|
|
22635
|
+
this.channelRegistry!.handleChannelToolCall(call.name, call.input, {
|
|
22636
|
+
kind: 'agent',
|
|
22637
|
+
agentName,
|
|
22638
|
+
// The turn's recorded servers go only with a call made inside that
|
|
22639
|
+
// turn: a background script's call carries no turn, and takes a bare
|
|
22640
|
+
// id only where exactly one server registers it.
|
|
22641
|
+
...(inTurn && callingTurn.triggerOrigin !== undefined ? { trigger: callingTurn.triggerOrigin } : {}),
|
|
22642
|
+
...(inTurn && callingTurn.locusOrigin !== undefined ? { locus: callingTurn.locusOrigin } : {}),
|
|
22643
|
+
})
|
|
22480
22644
|
.then((result) => {
|
|
22481
22645
|
const durationMs = Date.now() - startTime;
|
|
22482
22646
|
this.emitTrace({
|
|
@@ -22514,7 +22678,13 @@ export class AgentFramework {
|
|
|
22514
22678
|
if (openerAgent
|
|
22515
22679
|
&& (openerAgent.proseRouting === 'locus' || openerAgent.proseRouting === 'hybrid')
|
|
22516
22680
|
&& this.openLogical.get(agentName) === callingTurn) {
|
|
22517
|
-
|
|
22681
|
+
const openedOn = this.channelRegistry!.resolveDestination({
|
|
22682
|
+
...(typeof (call.input as { serverId?: unknown } | undefined)?.serverId === 'string'
|
|
22683
|
+
? { serverId: (call.input as { serverId: string }).serverId }
|
|
22684
|
+
: {}),
|
|
22685
|
+
channelId: opened,
|
|
22686
|
+
});
|
|
22687
|
+
setTurnLocus(callingTurn, opened, 'error' in openedOn ? undefined : openedOn.destination.serverId);
|
|
22518
22688
|
this.lastAnnouncedLocus.set(agentName, opened);
|
|
22519
22689
|
result = {
|
|
22520
22690
|
...result,
|
|
@@ -22788,17 +22958,21 @@ export class AgentFramework {
|
|
|
22788
22958
|
} else if (agent?.proseRouting === 'explicit') {
|
|
22789
22959
|
// Explicit mode: announce to the turn's sticky prose target if the
|
|
22790
22960
|
// model has set one; otherwise stay quiet — never guess a channel.
|
|
22791
|
-
const
|
|
22961
|
+
const routing = this.turnRouting.get(agentName);
|
|
22962
|
+
const target = routing?.pin;
|
|
22792
22963
|
if (target) {
|
|
22793
|
-
this.channelRegistry.routeSpeech(agentName, text, target
|
|
22964
|
+
this.channelRegistry.routeSpeech(agentName, text, target,
|
|
22965
|
+
speechServerOpts(call.callerTurnId === routing?.turnId ? routing : undefined, target)).catch((err) => {
|
|
22794
22966
|
console.error('[sleep] announce failed:', err instanceof Error ? err.message : err);
|
|
22795
22967
|
});
|
|
22796
22968
|
} else {
|
|
22797
22969
|
console.error(`[sleep] ${agentName}: no prose target this turn — sleep announcement not posted (explicit mode never guesses)`);
|
|
22798
22970
|
}
|
|
22799
22971
|
} else {
|
|
22800
|
-
const
|
|
22801
|
-
|
|
22972
|
+
const routing = this.turnRouting.get(agentName);
|
|
22973
|
+
const locus = routing?.locus ?? null;
|
|
22974
|
+
this.channelRegistry.routeSpeech(agentName, text, locus,
|
|
22975
|
+
speechServerOpts(call.callerTurnId === routing?.turnId ? routing : undefined, locus)).catch((err) => {
|
|
22802
22976
|
console.error('[sleep] announce failed:', err instanceof Error ? err.message : err);
|
|
22803
22977
|
});
|
|
22804
22978
|
}
|
package/src/gate/event-gate.ts
CHANGED
|
@@ -121,6 +121,9 @@ export interface WakeProvenance {
|
|
|
121
121
|
* (2026-09-30: a DM reply published into a guild channel). Ambient-only
|
|
122
122
|
* batches still set nothing and keep the legacy fallback. */
|
|
123
123
|
routeChannelId?: string;
|
|
124
|
+
/** The server whose event `routeChannelId` came from: the turn's replies
|
|
125
|
+
* to that channel go out through this server only. */
|
|
126
|
+
routeServerId?: string;
|
|
124
127
|
/** Timestamp (ms) of the chosen event, so a consumer coalescing several
|
|
125
128
|
* requests can order by event recency, not by flush order. */
|
|
126
129
|
at?: number;
|
|
@@ -226,7 +229,9 @@ export function wakeProvenance(events: PendingEvent[]): WakeProvenance | undefin
|
|
|
226
229
|
...(channelId ? { channelId } : {}),
|
|
227
230
|
...(pick.authorId ? { counterparty: `${ns}:user:${pick.authorId}` } : {}),
|
|
228
231
|
...(pick.addressed ? { addressed: true } : {}),
|
|
229
|
-
...(pick.addressed && pick.routeChannelId
|
|
232
|
+
...(pick.addressed && pick.routeChannelId
|
|
233
|
+
? { routeChannelId: pick.routeChannelId, ...(pick.serverId ? { routeServerId: pick.serverId } : {}) }
|
|
234
|
+
: {}),
|
|
230
235
|
at: pick.timestamp,
|
|
231
236
|
};
|
|
232
237
|
}
|