@animalabs/agent-framework 0.23.2 → 0.23.3
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 +191 -55
- 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 +94 -25
- 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 +4 -1
- package/src/framework.ts +219 -47
- package/src/gate/event-gate.ts +6 -1
- package/src/mcpl/channel-registry.ts +106 -24
- 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
|
@@ -801,6 +801,31 @@ interface TurnRouting {
|
|
|
801
801
|
* sent Sol's DM reply to a stale guild channel (2026-07-22).
|
|
802
802
|
*/
|
|
803
803
|
triggerChannel?: string;
|
|
804
|
+
/**
|
|
805
|
+
* The trigger's channel with the server whose message it was, frozen when
|
|
806
|
+
* the turn opens and never moved: a channel_open moves triggerChannel and
|
|
807
|
+
* a re-pin moves the locus, but neither carries this server with it. Every
|
|
808
|
+
* send of the turn whose destination id is this channel resolves with this
|
|
809
|
+
* server (triggerServerFor): speech, a channel_publish naming it or taking
|
|
810
|
+
* it as its default, a drafts resend, and the refusal reaction. So a
|
|
811
|
+
* message one server sent can't have its reply go out through another
|
|
812
|
+
* server that registers the same id, whatever the registry holds by then;
|
|
813
|
+
* the reply is refused where its server no longer registers the channel.
|
|
814
|
+
* Unset for a turn with no channel trigger, and where the requests batched
|
|
815
|
+
* into the trigger name its channel from more than one server: a bare id
|
|
816
|
+
* then resolves only where exactly one server registers it.
|
|
817
|
+
*/
|
|
818
|
+
triggerOrigin?: { channelId: string; serverId: string };
|
|
819
|
+
/**
|
|
820
|
+
* The locus with the server of the message that put it there, where that
|
|
821
|
+
* isn't the trigger: the latest accepted message's (the global fallback a
|
|
822
|
+
* turn with no trigger channel takes), an arrival's or a mid-turn
|
|
823
|
+
* message's (a re-pin), or the server a channel_open opened it on. Set
|
|
824
|
+
* with the locus (setTurnLocus), and cleared when the locus moves to a
|
|
825
|
+
* channel no one server put there. A send into this channel resolves with
|
|
826
|
+
* this server, as one into the trigger's channel does with the trigger's.
|
|
827
|
+
*/
|
|
828
|
+
locusOrigin?: { channelId: string; serverId: string };
|
|
804
829
|
/**
|
|
805
830
|
* Locus and hybrid mode: where the turn's unpinned plain prose lands.
|
|
806
831
|
* Frozen when the turn opens (home channel for a fork, else the trigger
|
|
@@ -877,11 +902,60 @@ interface Arrival {
|
|
|
877
902
|
messageId: MessageId;
|
|
878
903
|
/** The channel it re-pins the locus to. */
|
|
879
904
|
channelId: string;
|
|
905
|
+
/** The server whose message it is. */
|
|
906
|
+
serverId?: string;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/** The server a stored channel message came from, as the host wrote it. */
|
|
910
|
+
function messageServerId(metadata: MessageMetadata | undefined): string | undefined {
|
|
911
|
+
const serverId = (metadata as { serverId?: unknown } | undefined)?.serverId;
|
|
912
|
+
return typeof serverId === 'string' && serverId ? serverId : undefined;
|
|
880
913
|
}
|
|
881
914
|
|
|
882
915
|
/** The last TurnRouting.turnId given out: ids increase, and are unique within the process. */
|
|
883
916
|
let lastTurnId = 0;
|
|
884
917
|
|
|
918
|
+
/**
|
|
919
|
+
* The server every request naming `channelId` came from, or undefined where
|
|
920
|
+
* one names no server or they name more than one (TurnRouting.triggerOrigin).
|
|
921
|
+
*/
|
|
922
|
+
function onlyServerFor(requests: readonly InferenceRequest[], channelId: string): string | undefined {
|
|
923
|
+
let serverId: string | undefined;
|
|
924
|
+
for (const r of requests) {
|
|
925
|
+
if (r.channelId !== channelId) continue;
|
|
926
|
+
if (!r.serverId || (serverId !== undefined && r.serverId !== serverId)) return undefined;
|
|
927
|
+
serverId = r.serverId;
|
|
928
|
+
}
|
|
929
|
+
return serverId;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* The server a send in `routing`'s turn resolves `channelId` with: the
|
|
934
|
+
* trigger's, where `channelId` is the trigger's channel
|
|
935
|
+
* (TurnRouting.triggerOrigin), else the locus's, where it's the channel the
|
|
936
|
+
* locus's server put there (TurnRouting.locusOrigin).
|
|
937
|
+
*/
|
|
938
|
+
function triggerServerFor(routing: TurnRouting | undefined, channelId: string | null | undefined): string | undefined {
|
|
939
|
+
if (routing === undefined || !channelId) return undefined;
|
|
940
|
+
if (routing.triggerOrigin?.channelId === channelId) return routing.triggerOrigin.serverId;
|
|
941
|
+
if (routing.locusOrigin?.channelId === channelId) return routing.locusOrigin.serverId;
|
|
942
|
+
return undefined;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/** Move a turn's locus, with the server of the message that put it there
|
|
946
|
+
* (TurnRouting.locusOrigin), or none. */
|
|
947
|
+
function setTurnLocus(routing: TurnRouting, channelId: string, serverId: string | undefined): void {
|
|
948
|
+
routing.locus = channelId;
|
|
949
|
+
routing.locusOrigin = serverId !== undefined ? { channelId, serverId } : undefined;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/** routeSpeech's options for speech into `channelId` in `routing`'s turn:
|
|
953
|
+
* the trigger's server, where it's the trigger's channel (triggerServerFor). */
|
|
954
|
+
function speechServerOpts(routing: TurnRouting | undefined, channelId: string | null | undefined): { locusServerId: string } | undefined {
|
|
955
|
+
const serverId = triggerServerFor(routing, channelId);
|
|
956
|
+
return serverId !== undefined ? { locusServerId: serverId } : undefined;
|
|
957
|
+
}
|
|
958
|
+
|
|
885
959
|
function freshTurnRouting(): TurnRouting {
|
|
886
960
|
return {
|
|
887
961
|
turnId: ++lastTurnId,
|
|
@@ -2951,8 +3025,8 @@ export class AgentFramework {
|
|
|
2951
3025
|
// route applies to a fork only when it IS that home, and never to
|
|
2952
3026
|
// another agent when a fork owns that channel. Otherwise a fork
|
|
2953
3027
|
// bound elsewhere would show typing where it isn't replying.
|
|
2954
|
-
...(provenance?.routeChannelId && framework.mayTakeGateRoute(agentName, provenance.routeChannelId)
|
|
2955
|
-
? { channelId: provenance.routeChannelId, addressed: true }
|
|
3028
|
+
...(provenance?.routeChannelId && framework.mayTakeGateRoute(agentName, provenance.routeChannelId, provenance.routeServerId)
|
|
3029
|
+
? { channelId: provenance.routeChannelId, serverId: provenance.routeServerId, addressed: true }
|
|
2956
3030
|
: {}),
|
|
2957
3031
|
});
|
|
2958
3032
|
},
|
|
@@ -8508,7 +8582,9 @@ export class AgentFramework {
|
|
|
8508
8582
|
// tools/call directly on its connection (bare tool name — no prefix
|
|
8509
8583
|
// games), bypassing the agent event queue so no synthetic tool-result
|
|
8510
8584
|
// is injected into a turn the agent never took.
|
|
8511
|
-
|
|
8585
|
+
// Through the server whose message it marks: the latest accepted
|
|
8586
|
+
// message's, kept beside its channel and id (fallbackServerFor).
|
|
8587
|
+
const serverId = this.channelRegistry?.fallbackServerFor(incoming.channelId);
|
|
8512
8588
|
const server = serverId ? this.mcplServerRegistry?.getServer(serverId) : null;
|
|
8513
8589
|
if (!server) {
|
|
8514
8590
|
console.error(
|
|
@@ -11050,12 +11126,19 @@ export class AgentFramework {
|
|
|
11050
11126
|
// The last injection that qualifies wins (repinChannel).
|
|
11051
11127
|
const routing = this.routingOf(agent.name);
|
|
11052
11128
|
let newLocus: string | undefined;
|
|
11053
|
-
|
|
11129
|
+
let newLocusServer: string | undefined;
|
|
11130
|
+
for (const inj of midTurnInjections) {
|
|
11131
|
+
const channelId = this.repinChannel(inj.metadata, routing);
|
|
11132
|
+
if (channelId) {
|
|
11133
|
+
newLocus = channelId;
|
|
11134
|
+
newLocusServer = messageServerId(inj.metadata);
|
|
11135
|
+
}
|
|
11136
|
+
}
|
|
11054
11137
|
if (newLocus) {
|
|
11055
11138
|
// What this boundary just stored is newer than any arrival the
|
|
11056
11139
|
// turn holds, so the arrivals can't move the locus back.
|
|
11057
11140
|
routing.arrivals = [];
|
|
11058
|
-
const notice = this.repinTurnLocus(agent, routing, newLocus, 'mid-turn addressed');
|
|
11141
|
+
const notice = this.repinTurnLocus(agent, routing, newLocus, 'mid-turn addressed', newLocusServer);
|
|
11059
11142
|
if (notice) midTurnInjections.push(notice);
|
|
11060
11143
|
}
|
|
11061
11144
|
} else if (routesLocus && !shouldEndTurn) {
|
|
@@ -11440,14 +11523,15 @@ export class AgentFramework {
|
|
|
11440
11523
|
const id = target.getContextManager().addMessage('user', event.content, metadata);
|
|
11441
11524
|
// A correction may target an older engagement. Refresh only the
|
|
11442
11525
|
// binding whose agent actually received the message.
|
|
11443
|
-
|
|
11444
|
-
|
|
11526
|
+
const channel = { serverId: event.serverId, channelId: event.channelId };
|
|
11527
|
+
if (this.conversationRouter.getBinding(channel)?.agentName === target.name) {
|
|
11528
|
+
this.conversationRouter.touch(channel);
|
|
11445
11529
|
}
|
|
11446
11530
|
this.emitTrace({ type: 'message:added', messageId: id, source: 'mcpl:channel-incoming' });
|
|
11447
11531
|
if (event.triggerInference) {
|
|
11448
11532
|
this.pendingRequests.push({
|
|
11449
11533
|
agentName: target.name, reason: 'mcpl:channel-incoming', source: event.serverId, timestamp: Date.now(),
|
|
11450
|
-
channelId: event.channelId,
|
|
11534
|
+
channelId: event.channelId, serverId: event.serverId,
|
|
11451
11535
|
counterparty: event.author?.id ? `${event.serverId}:user:${event.author.id}` : undefined,
|
|
11452
11536
|
addressed: isAddressedMessage(event.tags, event.metadata),
|
|
11453
11537
|
...(event.coalescingSubject ? { coalescingSubject: event.coalescingSubject } : {}),
|
|
@@ -11526,8 +11610,10 @@ export class AgentFramework {
|
|
|
11526
11610
|
source: event.serverId,
|
|
11527
11611
|
timestamp: Date.now(),
|
|
11528
11612
|
// Route this turn's auto-published speech back to THIS channel, not
|
|
11529
|
-
// the global most-recent-inbound locus (item-3 redux, trunk agents)
|
|
11613
|
+
// the global most-recent-inbound locus (item-3 redux, trunk agents),
|
|
11614
|
+
// through the server that sent it.
|
|
11530
11615
|
channelId: event.channelId,
|
|
11616
|
+
serverId: event.serverId,
|
|
11531
11617
|
counterparty: event.author?.id ? `${event.serverId}:user:${event.author.id}` : undefined,
|
|
11532
11618
|
// Addressed messages outrank ambient chatter when a batched wake
|
|
11533
11619
|
// picks the turn's frozen speech locus.
|
|
@@ -11605,7 +11691,7 @@ export class AgentFramework {
|
|
|
11605
11691
|
if (!agent) {
|
|
11606
11692
|
// Bound agent vanished (e.g. external reset) — unbind so the next
|
|
11607
11693
|
// qualifying message respawns a fresh fork.
|
|
11608
|
-
router.unbind(event.channelId);
|
|
11694
|
+
router.unbind({ serverId: event.serverId, channelId: event.channelId });
|
|
11609
11695
|
this.emitTrace({
|
|
11610
11696
|
type: 'mcpl:conversation-binding-orphaned',
|
|
11611
11697
|
channelId: event.channelId,
|
|
@@ -11621,7 +11707,7 @@ export class AgentFramework {
|
|
|
11621
11707
|
messageMetadata.triggered = trigger;
|
|
11622
11708
|
|
|
11623
11709
|
const id = agent.getContextManager().addMessage('user', event.content, messageMetadata);
|
|
11624
|
-
router.touch(event.channelId);
|
|
11710
|
+
router.touch({ serverId: event.serverId, channelId: event.channelId });
|
|
11625
11711
|
this.emitTrace({ type: 'message:added', messageId: id, source: 'mcpl:channel-incoming' });
|
|
11626
11712
|
|
|
11627
11713
|
if (trigger) {
|
|
@@ -11633,6 +11719,7 @@ export class AgentFramework {
|
|
|
11633
11719
|
// A fork's home channel wins in routeSpeech regardless, but carry the
|
|
11634
11720
|
// triggering channel too so the trunk/active path stays consistent.
|
|
11635
11721
|
channelId: event.channelId,
|
|
11722
|
+
serverId: event.serverId,
|
|
11636
11723
|
counterparty: event.author?.id ? `${event.serverId}:user:${event.author.id}` : undefined,
|
|
11637
11724
|
addressed: isAddressedMessage(event.tags, event.metadata),
|
|
11638
11725
|
...(event.coalescingSubject ? { coalescingSubject: event.coalescingSubject } : {}),
|
|
@@ -11777,7 +11864,7 @@ export class AgentFramework {
|
|
|
11777
11864
|
}
|
|
11778
11865
|
const decision = this.conversationRouter!.route(facts);
|
|
11779
11866
|
if (decision.kind !== 'spawn') return decision;
|
|
11780
|
-
return { ...decision, spawned: this.spawnConversationAgent(facts
|
|
11867
|
+
return { ...decision, spawned: this.spawnConversationAgent(facts, decision) };
|
|
11781
11868
|
}
|
|
11782
11869
|
}
|
|
11783
11870
|
|
|
@@ -11793,11 +11880,12 @@ export class AgentFramework {
|
|
|
11793
11880
|
* so the entry it removes is its own. It's removed before anyone waiting
|
|
11794
11881
|
* on it resumes, so a waiter that decides again doesn't find it.
|
|
11795
11882
|
*/
|
|
11796
|
-
private spawnConversationAgent(
|
|
11883
|
+
private spawnConversationAgent(facts: IncomingMessageFacts, decision: { agentName: string; generation: number }): Promise<Agent> {
|
|
11884
|
+
const channelId = facts.channelId;
|
|
11797
11885
|
const spawn = (async () => {
|
|
11798
11886
|
this.storeConversationGeneration(channelId, decision.generation);
|
|
11799
11887
|
const agent = await this.createConversationAgent(decision.agentName, channelId);
|
|
11800
|
-
this.conversationRouter!.bind(channelId, decision.agentName, decision.generation);
|
|
11888
|
+
this.conversationRouter!.bind({ serverId: facts.serverId, channelId }, decision.agentName, decision.generation);
|
|
11801
11889
|
return agent;
|
|
11802
11890
|
})();
|
|
11803
11891
|
const done = (): void => { this.conversationSpawns.delete(channelId); };
|
|
@@ -11927,7 +12015,7 @@ export class AgentFramework {
|
|
|
11927
12015
|
this.lastConversationSweep = now;
|
|
11928
12016
|
|
|
11929
12017
|
for (const binding of this.conversationRouter.expired(now)) {
|
|
11930
|
-
this.conversationRouter.unbind(binding
|
|
12018
|
+
this.conversationRouter.unbind(binding);
|
|
11931
12019
|
this.emitTrace({
|
|
11932
12020
|
type: 'mcpl:conversation-closed',
|
|
11933
12021
|
channelId: binding.channelId,
|
|
@@ -12607,7 +12695,7 @@ export class AgentFramework {
|
|
|
12607
12695
|
if (explicit) return explicit.filter((n) => this.agents.has(n));
|
|
12608
12696
|
if (occ.scope.kind === 'channel' && this.conversationRouter) {
|
|
12609
12697
|
const router = this.conversationRouter;
|
|
12610
|
-
const bound = router.getBinding(occ.scope.id)?.agentName;
|
|
12698
|
+
const bound = router.getBinding({ serverId: occ.serverId, channelId: occ.scope.id })?.agentName;
|
|
12611
12699
|
if (bound) return this.agents.has(bound) ? [bound] : [];
|
|
12612
12700
|
// No fork yet: the same bind decision an incoming message would get. A
|
|
12613
12701
|
// batch that qualifies spawns its fork now so it has someone to wake.
|
|
@@ -12723,6 +12811,7 @@ export class AgentFramework {
|
|
|
12723
12811
|
source: occ.serverId,
|
|
12724
12812
|
timestamp: Date.now(),
|
|
12725
12813
|
channelId,
|
|
12814
|
+
serverId: channelId !== undefined ? occ.serverId : undefined,
|
|
12726
12815
|
counterparty: authorId ? `${occ.serverId}:user:${authorId}` : undefined,
|
|
12727
12816
|
addressed: isAddressedMessage(occ.tags, origin),
|
|
12728
12817
|
...(plain ? {} : { coalescingSubject: subject, coalescingBatch: true }),
|
|
@@ -12936,6 +13025,7 @@ export class AgentFramework {
|
|
|
12936
13025
|
source: event.serverId,
|
|
12937
13026
|
timestamp: Date.now(),
|
|
12938
13027
|
channelId: triggerChannel?.channelId,
|
|
13028
|
+
serverId: triggerChannel ? event.serverId : undefined,
|
|
12939
13029
|
// DMs and addressed-while-closed messages arrive as push events;
|
|
12940
13030
|
// they must outrank ambient chatter in a batched wake's locus
|
|
12941
13031
|
// selection just like their channels/incoming counterparts.
|
|
@@ -12960,17 +13050,49 @@ export class AgentFramework {
|
|
|
12960
13050
|
* Whether a batched gate wake's addressed route may become `agentName`'s
|
|
12961
13051
|
* speech locus. The gate broadcasts to every agent, conversation forks
|
|
12962
13052
|
* included. A fork speaks only in its home channel, and a channel bound to
|
|
12963
|
-
* a fork is that fork's conversation, so no other agent takes it.
|
|
13053
|
+
* a fork is that fork's conversation, so no other agent takes it. Two
|
|
13054
|
+
* servers' channels with one id are two conversations, each bound to its
|
|
13055
|
+
* own fork with that id as its home, so a fork takes the route only where
|
|
13056
|
+
* the router binds the route's channel, its server and id, to it; a route
|
|
13057
|
+
* with no server names a fork only where one fork is bound to that id.
|
|
12964
13058
|
*/
|
|
12965
|
-
private mayTakeGateRoute(agentName: string, routeChannelId: string): boolean {
|
|
13059
|
+
private mayTakeGateRoute(agentName: string, routeChannelId: string, routeServerId?: string): boolean {
|
|
12966
13060
|
const home = this.conversationAgentHomes.get(agentName);
|
|
12967
|
-
if (home !== undefined)
|
|
13061
|
+
if (home !== undefined) {
|
|
13062
|
+
return home === routeChannelId
|
|
13063
|
+
&& this.conversationRouter?.getBinding({ serverId: routeServerId, channelId: routeChannelId })?.agentName === agentName;
|
|
13064
|
+
}
|
|
12968
13065
|
for (const boundChannel of this.conversationAgentHomes.values()) {
|
|
12969
13066
|
if (boundChannel === routeChannelId) return false;
|
|
12970
13067
|
}
|
|
12971
13068
|
return true;
|
|
12972
13069
|
}
|
|
12973
13070
|
|
|
13071
|
+
/**
|
|
13072
|
+
* The channel a push names, and a server that registers it, when another
|
|
13073
|
+
* server registers that channel id and the pushing server doesn't. A
|
|
13074
|
+
* channel's id is the agent's locus on its own, so the reply to such a
|
|
13075
|
+
* push would go out through the server that registers it: the push
|
|
13076
|
+
* boundary refuses it (PushHandler). A push naming a channel no server
|
|
13077
|
+
* registers isn't refused, and registers it for the pushing server
|
|
13078
|
+
* (ensureChannelRegistered).
|
|
13079
|
+
*/
|
|
13080
|
+
private channelOfAnotherServer(
|
|
13081
|
+
serverId: string,
|
|
13082
|
+
origin: Record<string, unknown> | undefined,
|
|
13083
|
+
): { channelId: string; holder: string } | undefined {
|
|
13084
|
+
if (!this.channelRegistry) return undefined;
|
|
13085
|
+
const channel = this.derivePushEventChannel(origin);
|
|
13086
|
+
if (!channel) return undefined;
|
|
13087
|
+
let holder: string | undefined;
|
|
13088
|
+
for (const entry of this.channelRegistry.listChannelsRaw()) {
|
|
13089
|
+
if (entry.descriptor.id !== channel.channelId) continue;
|
|
13090
|
+
if (entry.serverId === serverId) return undefined;
|
|
13091
|
+
holder ??= entry.serverId;
|
|
13092
|
+
}
|
|
13093
|
+
return holder === undefined ? undefined : { channelId: channel.channelId, holder };
|
|
13094
|
+
}
|
|
13095
|
+
|
|
12974
13096
|
/**
|
|
12975
13097
|
* Derive the MCPL composite channel id (the outbound routing locus) for a
|
|
12976
13098
|
* push event from its server-defined `origin`, if it carries one.
|
|
@@ -13302,6 +13424,7 @@ export class AgentFramework {
|
|
|
13302
13424
|
suppressProse: silentOnly ? trigger?.suppressProse : undefined,
|
|
13303
13425
|
silentHeartbeat: silentOnly ? trigger?.silentHeartbeat : undefined,
|
|
13304
13426
|
channelId: channelReq?.channelId,
|
|
13427
|
+
serverId: channelReq?.channelId !== undefined ? onlyServerFor(requests, channelReq.channelId) : undefined,
|
|
13305
13428
|
addressed: addressedReq !== undefined,
|
|
13306
13429
|
// A context-budget restart continues the same logical turn: it keeps
|
|
13307
13430
|
// the channel for routing but names no author — the restart is its
|
|
@@ -13769,8 +13892,8 @@ export class AgentFramework {
|
|
|
13769
13892
|
/** The drafts tool's answer to a resident's call, from the model's own
|
|
13770
13893
|
* dispatch or an agent-origin call through executeToolCall (a puppet's):
|
|
13771
13894
|
* a failure the handler throws is answered as one. */
|
|
13772
|
-
private draftsToolResult(agentName: string, rawInput: unknown): Promise<ToolResult> {
|
|
13773
|
-
return this.handleDraftsTool(agentName, rawInput).catch((err): ToolResult => ({
|
|
13895
|
+
private draftsToolResult(agentName: string, rawInput: unknown, callerTurnId?: number): Promise<ToolResult> {
|
|
13896
|
+
return this.handleDraftsTool(agentName, rawInput, callerTurnId).catch((err): ToolResult => ({
|
|
13774
13897
|
success: false,
|
|
13775
13898
|
error: `drafts failed: ${thrownText(err)}`,
|
|
13776
13899
|
isError: true,
|
|
@@ -13783,7 +13906,7 @@ export class AgentFramework {
|
|
|
13783
13906
|
* each draft is published verbatim, in order, through the normal publish
|
|
13784
13907
|
* executor, its attempt journaled durably before the request leaves.
|
|
13785
13908
|
*/
|
|
13786
|
-
private async handleDraftsTool(agentName: string, rawInput: unknown): Promise<ToolResult> {
|
|
13909
|
+
private async handleDraftsTool(agentName: string, rawInput: unknown, callerTurnId?: number): Promise<ToolResult> {
|
|
13787
13910
|
const input = (rawInput && typeof rawInput === 'object' && !Array.isArray(rawInput) ? rawInput : {}) as Record<string, unknown>;
|
|
13788
13911
|
// The turn that sends: a resent draft's channel is recorded as engaged on
|
|
13789
13912
|
// it, as a send tool's is, even when the result comes after the turn.
|
|
@@ -13950,7 +14073,17 @@ export class AgentFramework {
|
|
|
13950
14073
|
const cand = resolved.candidates?.length ? ` It could mean: ${resolved.candidates.join(', ')}.` : '';
|
|
13951
14074
|
return refuse(`Destination "${spec}" did not resolve: ${resolved.error}.${cand} Nothing was sent.`);
|
|
13952
14075
|
}
|
|
13953
|
-
|
|
14076
|
+
// A resend into a channel the calling turn recorded a server for goes
|
|
14077
|
+
// through that server (triggerServerFor), where the call was made inside
|
|
14078
|
+
// that turn; a background script's call carries no turn.
|
|
14079
|
+
const callingRouting = this.turnRouting.get(agentName);
|
|
14080
|
+
const triggerServer = callerTurnId !== undefined && callingRouting?.turnId === callerTurnId
|
|
14081
|
+
? triggerServerFor(callingRouting, resolved.channelId)
|
|
14082
|
+
: undefined;
|
|
14083
|
+
const target = registry.resolveDestination({
|
|
14084
|
+
...(triggerServer !== undefined ? { serverId: triggerServer } : {}),
|
|
14085
|
+
channelId: resolved.channelId,
|
|
14086
|
+
});
|
|
13954
14087
|
if ('error' in target) return refuse(`Destination "${spec}": ${target.error}. Nothing was sent.`);
|
|
13955
14088
|
const destination = target.destination;
|
|
13956
14089
|
if (home && destination.channelId !== home) {
|
|
@@ -14303,7 +14436,10 @@ export class AgentFramework {
|
|
|
14303
14436
|
if (!open) return;
|
|
14304
14437
|
if ((metadata as { tuneOut?: unknown } | undefined)?.tuneOut) return;
|
|
14305
14438
|
const channelId = this.repinChannel(metadata, open);
|
|
14306
|
-
if (channelId)
|
|
14439
|
+
if (channelId) {
|
|
14440
|
+
const serverId = messageServerId(metadata);
|
|
14441
|
+
open.arrivals.push({ messageId, channelId, ...(serverId !== undefined ? { serverId } : {}) });
|
|
14442
|
+
}
|
|
14307
14443
|
}
|
|
14308
14444
|
|
|
14309
14445
|
/**
|
|
@@ -14339,14 +14475,14 @@ export class AgentFramework {
|
|
|
14339
14475
|
* message in the store; the last compile can't be the test, since an
|
|
14340
14476
|
* arrival between the turn's streams isn't in it yet.
|
|
14341
14477
|
*/
|
|
14342
|
-
private remainingArrival(agent: Agent, routing: TurnRouting):
|
|
14478
|
+
private remainingArrival(agent: Agent, routing: TurnRouting): Arrival | undefined {
|
|
14343
14479
|
const cm = agent.getContextManager();
|
|
14344
14480
|
for (let i = routing.arrivals.length - 1; i >= 0; i--) {
|
|
14345
|
-
const
|
|
14481
|
+
const arrival = routing.arrivals[i]!;
|
|
14346
14482
|
// The store answers null for a message that isn't there. A store that
|
|
14347
14483
|
// can't be read throws here, as anywhere else in the start, rather
|
|
14348
14484
|
// than have its candidates read as withdrawn.
|
|
14349
|
-
if (cm.getMessage(messageId)) return
|
|
14485
|
+
if (cm.getMessage(arrival.messageId)) return arrival;
|
|
14350
14486
|
}
|
|
14351
14487
|
return undefined;
|
|
14352
14488
|
}
|
|
@@ -14368,10 +14504,12 @@ export class AgentFramework {
|
|
|
14368
14504
|
routing: TurnRouting,
|
|
14369
14505
|
channelId: string,
|
|
14370
14506
|
cause: string,
|
|
14507
|
+
/** The server of the message that moved it. */
|
|
14508
|
+
serverId?: string,
|
|
14371
14509
|
): { participant: string; content: ContentBlock[]; metadata: MessageMetadata } | undefined {
|
|
14372
14510
|
if (routing.locus === channelId) return undefined;
|
|
14373
14511
|
const prevPin = routing.locus ?? null;
|
|
14374
|
-
routing
|
|
14512
|
+
setTurnLocus(routing, channelId, serverId);
|
|
14375
14513
|
const label = this.channelRegistry?.getDescriptor(channelId)?.label;
|
|
14376
14514
|
const shown = label && label !== channelId
|
|
14377
14515
|
? `${label.startsWith('#') ? label : `#${label}`} (${channelId})`
|
|
@@ -14657,11 +14795,13 @@ export class AgentFramework {
|
|
|
14657
14795
|
body: string,
|
|
14658
14796
|
channelId: string,
|
|
14659
14797
|
unsent: Draft | undefined,
|
|
14798
|
+
/** The server the channel resolves with (triggerServerFor). */
|
|
14799
|
+
serverId: string | undefined,
|
|
14660
14800
|
): Promise<PublishOutcome> {
|
|
14661
14801
|
const registry = this.channelRegistry!;
|
|
14662
14802
|
let attemptId: string | undefined;
|
|
14663
14803
|
if (unsent) {
|
|
14664
|
-
const target = registry.resolveDestination({ channelId });
|
|
14804
|
+
const target = registry.resolveDestination({ ...(serverId !== undefined ? { serverId } : {}), channelId });
|
|
14665
14805
|
if (!('error' in target)) {
|
|
14666
14806
|
try {
|
|
14667
14807
|
attemptId = this.proseDrafts.beginAttempt(agent.name, unsent.id, target.destination, 'unsent-token', false);
|
|
@@ -14682,7 +14822,8 @@ export class AgentFramework {
|
|
|
14682
14822
|
try {
|
|
14683
14823
|
// Both callers hold a failed delivery's words as a bounce draft, whose
|
|
14684
14824
|
// notice says what happened, so the registry writes no second marker.
|
|
14685
|
-
outcome = await registry.deliverSpeech(agent.name, body, channelId,
|
|
14825
|
+
outcome = await registry.deliverSpeech(agent.name, body, channelId,
|
|
14826
|
+
{ heldOnFailure: true, ...(serverId !== undefined ? { locusServerId: serverId } : {}) });
|
|
14686
14827
|
} finally {
|
|
14687
14828
|
if (flight) {
|
|
14688
14829
|
this.draftsInFlight.delete(flight);
|
|
@@ -14781,7 +14922,7 @@ export class AgentFramework {
|
|
|
14781
14922
|
continue;
|
|
14782
14923
|
}
|
|
14783
14924
|
try {
|
|
14784
|
-
const outcome = await this.deliverWithUnsent(agent, body, resolved.channelId, unsent.draft);
|
|
14925
|
+
const outcome = await this.deliverWithUnsent(agent, body, resolved.channelId, unsent.draft, triggerServerFor(routing, resolved.channelId));
|
|
14785
14926
|
this.recordProseDelivery(routing, outcome.status === 'delivered' ? { delivered: true, channelId: outcome.destination!.channelId } : null);
|
|
14786
14927
|
if (outcome.status === 'delivered') {
|
|
14787
14928
|
this.proseBounceStreaks.delete(agent.name);
|
|
@@ -14823,7 +14964,7 @@ export class AgentFramework {
|
|
|
14823
14964
|
}
|
|
14824
14965
|
const target = sticky ?? locus;
|
|
14825
14966
|
try {
|
|
14826
|
-
const outcome = await this.channelRegistry!.routeSpeech(agent.name, envelope, target);
|
|
14967
|
+
const outcome = await this.channelRegistry!.routeSpeech(agent.name, envelope, target, speechServerOpts(routing, target));
|
|
14827
14968
|
this.recordProseDelivery(routing, outcome);
|
|
14828
14969
|
} catch (err) {
|
|
14829
14970
|
console.error('hybrid locus delivery failed:', err);
|
|
@@ -14946,7 +15087,7 @@ export class AgentFramework {
|
|
|
14946
15087
|
}
|
|
14947
15088
|
|
|
14948
15089
|
try {
|
|
14949
|
-
const outcome = await this.deliverWithUnsent(agent, body, targetChannel, unsent.draft);
|
|
15090
|
+
const outcome = await this.deliverWithUnsent(agent, body, targetChannel, unsent.draft, triggerServerFor(routing, targetChannel));
|
|
14950
15091
|
if (outcome.status === 'delivered') {
|
|
14951
15092
|
this.proseBounceStreaks.delete(name);
|
|
14952
15093
|
} else if (outcome.status === 'failed') {
|
|
@@ -15662,6 +15803,9 @@ export class AgentFramework {
|
|
|
15662
15803
|
// turn, which falls back to the global default rather than inheriting
|
|
15663
15804
|
// a previous turn's channel.
|
|
15664
15805
|
routing.triggerChannel = trigger?.channelId;
|
|
15806
|
+
routing.triggerOrigin = trigger?.channelId && trigger.serverId
|
|
15807
|
+
? { channelId: trigger.channelId, serverId: trigger.serverId }
|
|
15808
|
+
: undefined;
|
|
15665
15809
|
// FREEZE the turn's outbound locus now (turn-frozen routing): the home
|
|
15666
15810
|
// channel for forks, else the trigger channel, else the global default,
|
|
15667
15811
|
// which an ephemeral run never takes (takesGlobalFallback).
|
|
@@ -15677,7 +15821,14 @@ export class AgentFramework {
|
|
|
15677
15821
|
// stability).
|
|
15678
15822
|
if (turnProseRouting !== 'explicit' && turnProseRouting !== 'disabled') {
|
|
15679
15823
|
const locus = this.channelRegistry?.resolveLocus(agent.name) ?? null;
|
|
15680
|
-
|
|
15824
|
+
// A locus that is neither the trigger's channel nor the agent's home
|
|
15825
|
+
// is the global fallback: its server is the latest accepted
|
|
15826
|
+
// message's, taken now, with the locus.
|
|
15827
|
+
if (locus !== null) {
|
|
15828
|
+
setTurnLocus(routing, locus, locus !== routing.triggerChannel && !this.conversationAgentHomes.has(agent.name)
|
|
15829
|
+
? this.channelRegistry?.fallbackServerFor(locus)
|
|
15830
|
+
: undefined);
|
|
15831
|
+
}
|
|
15681
15832
|
if (attempt === 0) this.announceLocusIfChanged(agent.name, locus);
|
|
15682
15833
|
}
|
|
15683
15834
|
} else {
|
|
@@ -15690,7 +15841,7 @@ export class AgentFramework {
|
|
|
15690
15841
|
? this.remainingArrival(agent, routing)
|
|
15691
15842
|
: undefined;
|
|
15692
15843
|
routing.arrivals = [];
|
|
15693
|
-
if (arrival) this.repinTurnLocus(agent, routing, arrival, 'arrival');
|
|
15844
|
+
if (arrival) this.repinTurnLocus(agent, routing, arrival.channelId, 'arrival', arrival.serverId);
|
|
15694
15845
|
}
|
|
15695
15846
|
// A silent start, or a disabled agent's, has no locus to show, whatever a
|
|
15696
15847
|
// channel_open in a silent turn left on its object.
|
|
@@ -16243,7 +16394,7 @@ export class AgentFramework {
|
|
|
16243
16394
|
};
|
|
16244
16395
|
const enqueueSpeech = (text: string, locus: string | null): void => {
|
|
16245
16396
|
chainSpeech(async () => {
|
|
16246
|
-
const outcome = await this.channelRegistry!.routeSpeech(agent.name, text, locus);
|
|
16397
|
+
const outcome = await this.channelRegistry!.routeSpeech(agent.name, text, locus, speechServerOpts(streamRouting, locus));
|
|
16247
16398
|
this.recordProseDelivery(streamRouting, outcome);
|
|
16248
16399
|
}, 'mid-turn speech routing failed:');
|
|
16249
16400
|
};
|
|
@@ -16366,6 +16517,7 @@ export class AgentFramework {
|
|
|
16366
16517
|
for (const rd of deltas) {
|
|
16367
16518
|
this.channelRegistry!.sendOutgoingChunk(
|
|
16368
16519
|
rd.channelId, agent.name, outgoingInferenceId, outgoingIndex++, rd.delta,
|
|
16520
|
+
triggerServerFor(streamRouting, rd.channelId),
|
|
16369
16521
|
);
|
|
16370
16522
|
}
|
|
16371
16523
|
};
|
|
@@ -17288,7 +17440,7 @@ export class AgentFramework {
|
|
|
17288
17440
|
);
|
|
17289
17441
|
for (const segment of speechSegments) {
|
|
17290
17442
|
try {
|
|
17291
|
-
const outcome = await this.channelRegistry.routeSpeech(agent.name, segment, locus);
|
|
17443
|
+
const outcome = await this.channelRegistry.routeSpeech(agent.name, segment, locus, speechServerOpts(streamRouting, locus));
|
|
17292
17444
|
this.recordProseDelivery(streamRouting, outcome);
|
|
17293
17445
|
} catch (err) {
|
|
17294
17446
|
console.error('speech routing failed:', err);
|
|
@@ -17385,7 +17537,7 @@ export class AgentFramework {
|
|
|
17385
17537
|
// Deliver sequentially (await each) so the segments land in order.
|
|
17386
17538
|
for (const seg of segments) {
|
|
17387
17539
|
try {
|
|
17388
|
-
const outcome = await this.channelRegistry.routeSpeech(agent.name, seg, locus);
|
|
17540
|
+
const outcome = await this.channelRegistry.routeSpeech(agent.name, seg, locus, speechServerOpts(streamRouting, locus));
|
|
17389
17541
|
this.recordProseDelivery(streamRouting, outcome);
|
|
17390
17542
|
} catch (err) {
|
|
17391
17543
|
console.error('speech routing failed:', err);
|
|
@@ -18043,7 +18195,7 @@ export class AgentFramework {
|
|
|
18043
18195
|
if (frameReachedTerminal && proseStream) {
|
|
18044
18196
|
emitOutgoing(proseStream.finish());
|
|
18045
18197
|
for (const [channelId, text] of proseStream.byChannel()) {
|
|
18046
|
-
this.channelRegistry!.sendOutgoingComplete(channelId, agent.name, outgoingInferenceId, text);
|
|
18198
|
+
this.channelRegistry!.sendOutgoingComplete(channelId, agent.name, outgoingInferenceId, text, triggerServerFor(streamRouting, channelId));
|
|
18047
18199
|
}
|
|
18048
18200
|
}
|
|
18049
18201
|
this.frameworkCancelledStreams.delete(`${agent.name}:${myStreamId}`);
|
|
@@ -18347,7 +18499,7 @@ export class AgentFramework {
|
|
|
18347
18499
|
// agent origin and acts as that resident, as the model's own call does
|
|
18348
18500
|
// through dispatchToolCall. A module holds no drafts.
|
|
18349
18501
|
if (call.name === 'drafts' && this.channelRegistry && origin.kind === 'agent') {
|
|
18350
|
-
return this.draftsToolResult(origin.agentName, call.input);
|
|
18502
|
+
return this.draftsToolResult(origin.agentName, call.input, call.callerTurnId);
|
|
18351
18503
|
}
|
|
18352
18504
|
|
|
18353
18505
|
// The utils meta-tool — programmatic callers (code_execution scripts,
|
|
@@ -19942,7 +20094,7 @@ export class AgentFramework {
|
|
|
19942
20094
|
if (enrichedCall.name === 'drafts' && this.channelRegistry) {
|
|
19943
20095
|
const startedAt = Date.now();
|
|
19944
20096
|
this.emitTrace({ type: 'tool:started', module: 'framework', tool: 'drafts', callId: enrichedCall.id, input: enrichedCall.input });
|
|
19945
|
-
void this.draftsToolResult(agentName, enrichedCall.input)
|
|
20097
|
+
void this.draftsToolResult(agentName, enrichedCall.input, enrichedCall.callerTurnId)
|
|
19946
20098
|
.then((result) => {
|
|
19947
20099
|
this.emitTrace({ type: 'tool:completed', module: 'framework', tool: 'drafts', callId: enrichedCall.id, durationMs: Date.now() - startedAt });
|
|
19948
20100
|
this.pushEvent({ type: 'tool-result', callId: enrichedCall.id, toolName: enrichedCall.name, agentName, moduleName: 'framework', result });
|
|
@@ -21491,6 +21643,7 @@ export class AgentFramework {
|
|
|
21491
21643
|
this.noteInboundAccepted(source);
|
|
21492
21644
|
return source;
|
|
21493
21645
|
},
|
|
21646
|
+
(serverId, params) => this.channelOfAnotherServer(serverId, params.origin),
|
|
21494
21647
|
);
|
|
21495
21648
|
|
|
21496
21649
|
// Server-initiated inference router (Step 6)
|
|
@@ -25126,8 +25279,17 @@ export class AgentFramework {
|
|
|
25126
25279
|
// ordinary wake, starts only once its over-budget boundary has every
|
|
25127
25280
|
// result of the round.
|
|
25128
25281
|
const callingTurn = this.routingOf(agentName);
|
|
25282
|
+
const inTurn = call.callerTurnId !== undefined && call.callerTurnId === callingTurn.turnId;
|
|
25129
25283
|
|
|
25130
|
-
this.channelRegistry!.handleChannelToolCall(call.name, call.input, {
|
|
25284
|
+
this.channelRegistry!.handleChannelToolCall(call.name, call.input, {
|
|
25285
|
+
kind: 'agent',
|
|
25286
|
+
agentName,
|
|
25287
|
+
// The turn's recorded servers go only with a call made inside that
|
|
25288
|
+
// turn: a background script's call carries no turn, and takes a bare
|
|
25289
|
+
// id only where exactly one server registers it.
|
|
25290
|
+
...(inTurn && callingTurn.triggerOrigin !== undefined ? { trigger: callingTurn.triggerOrigin } : {}),
|
|
25291
|
+
...(inTurn && callingTurn.locusOrigin !== undefined ? { locus: callingTurn.locusOrigin } : {}),
|
|
25292
|
+
})
|
|
25131
25293
|
.then((result) => {
|
|
25132
25294
|
const durationMs = Date.now() - startTime;
|
|
25133
25295
|
this.emitTrace({
|
|
@@ -25165,7 +25327,13 @@ export class AgentFramework {
|
|
|
25165
25327
|
if (openerAgent
|
|
25166
25328
|
&& (openerAgent.proseRouting === 'locus' || openerAgent.proseRouting === 'hybrid')
|
|
25167
25329
|
&& this.openLogical.get(agentName) === callingTurn) {
|
|
25168
|
-
|
|
25330
|
+
const openedOn = this.channelRegistry!.resolveDestination({
|
|
25331
|
+
...(typeof (call.input as { serverId?: unknown } | undefined)?.serverId === 'string'
|
|
25332
|
+
? { serverId: (call.input as { serverId: string }).serverId }
|
|
25333
|
+
: {}),
|
|
25334
|
+
channelId: opened,
|
|
25335
|
+
});
|
|
25336
|
+
setTurnLocus(callingTurn, opened, 'error' in openedOn ? undefined : openedOn.destination.serverId);
|
|
25169
25337
|
this.lastAnnouncedLocus.set(agentName, opened);
|
|
25170
25338
|
result = {
|
|
25171
25339
|
...result,
|
|
@@ -25439,17 +25607,21 @@ export class AgentFramework {
|
|
|
25439
25607
|
} else if (agent?.proseRouting === 'explicit') {
|
|
25440
25608
|
// Explicit mode: announce to the turn's sticky prose target if the
|
|
25441
25609
|
// model has set one; otherwise stay quiet — never guess a channel.
|
|
25442
|
-
const
|
|
25610
|
+
const routing = this.turnRouting.get(agentName);
|
|
25611
|
+
const target = routing?.pin;
|
|
25443
25612
|
if (target) {
|
|
25444
|
-
this.channelRegistry.routeSpeech(agentName, text, target
|
|
25613
|
+
this.channelRegistry.routeSpeech(agentName, text, target,
|
|
25614
|
+
speechServerOpts(call.callerTurnId === routing?.turnId ? routing : undefined, target)).catch((err) => {
|
|
25445
25615
|
console.error('[sleep] announce failed:', err instanceof Error ? err.message : err);
|
|
25446
25616
|
});
|
|
25447
25617
|
} else {
|
|
25448
25618
|
console.error(`[sleep] ${agentName}: no prose target this turn — sleep announcement not posted (explicit mode never guesses)`);
|
|
25449
25619
|
}
|
|
25450
25620
|
} else {
|
|
25451
|
-
const
|
|
25452
|
-
|
|
25621
|
+
const routing = this.turnRouting.get(agentName);
|
|
25622
|
+
const locus = routing?.locus ?? null;
|
|
25623
|
+
this.channelRegistry.routeSpeech(agentName, text, locus,
|
|
25624
|
+
speechServerOpts(call.callerTurnId === routing?.turnId ? routing : undefined, locus)).catch((err) => {
|
|
25453
25625
|
console.error('[sleep] announce failed:', err instanceof Error ? err.message : err);
|
|
25454
25626
|
});
|
|
25455
25627
|
}
|
package/src/gate/event-gate.ts
CHANGED
|
@@ -130,6 +130,9 @@ export interface WakeProvenance {
|
|
|
130
130
|
* (2026-09-30: a DM reply published into a guild channel). Ambient-only
|
|
131
131
|
* batches still set nothing and keep the legacy fallback. */
|
|
132
132
|
routeChannelId?: string;
|
|
133
|
+
/** The server whose event `routeChannelId` came from: the turn's replies
|
|
134
|
+
* to that channel go out through this server only. */
|
|
135
|
+
routeServerId?: string;
|
|
133
136
|
/** Timestamp (ms) of the chosen event, so a consumer coalescing several
|
|
134
137
|
* requests can order by event recency, not by flush order. */
|
|
135
138
|
at?: number;
|
|
@@ -235,7 +238,9 @@ export function wakeProvenance(events: PendingEvent[]): WakeProvenance | undefin
|
|
|
235
238
|
...(channelId ? { channelId } : {}),
|
|
236
239
|
...(pick.authorId ? { counterparty: `${ns}:user:${pick.authorId}` } : {}),
|
|
237
240
|
...(pick.addressed ? { addressed: true } : {}),
|
|
238
|
-
...(pick.addressed && pick.routeChannelId
|
|
241
|
+
...(pick.addressed && pick.routeChannelId
|
|
242
|
+
? { routeChannelId: pick.routeChannelId, ...(pick.serverId ? { routeServerId: pick.serverId } : {}) }
|
|
243
|
+
: {}),
|
|
239
244
|
at: pick.timestamp,
|
|
240
245
|
};
|
|
241
246
|
}
|