@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/dist/src/framework.js
CHANGED
|
@@ -489,8 +489,55 @@ function announcedDomainsOf(announce) {
|
|
|
489
489
|
function resumesLogicalTurn(reason) {
|
|
490
490
|
return reason === 'context_budget_restart' || reason === 'tool_result_guard_retry';
|
|
491
491
|
}
|
|
492
|
+
/** The server a stored channel message came from, as the host wrote it. */
|
|
493
|
+
function messageServerId(metadata) {
|
|
494
|
+
const serverId = metadata?.serverId;
|
|
495
|
+
return typeof serverId === 'string' && serverId ? serverId : undefined;
|
|
496
|
+
}
|
|
492
497
|
/** The last TurnRouting.turnId given out: ids increase, and are unique within the process. */
|
|
493
498
|
let lastTurnId = 0;
|
|
499
|
+
/**
|
|
500
|
+
* The server every request naming `channelId` came from, or undefined where
|
|
501
|
+
* one names no server or they name more than one (TurnRouting.triggerOrigin).
|
|
502
|
+
*/
|
|
503
|
+
function onlyServerFor(requests, channelId) {
|
|
504
|
+
let serverId;
|
|
505
|
+
for (const r of requests) {
|
|
506
|
+
if (r.channelId !== channelId)
|
|
507
|
+
continue;
|
|
508
|
+
if (!r.serverId || (serverId !== undefined && r.serverId !== serverId))
|
|
509
|
+
return undefined;
|
|
510
|
+
serverId = r.serverId;
|
|
511
|
+
}
|
|
512
|
+
return serverId;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* The server a send in `routing`'s turn resolves `channelId` with: the
|
|
516
|
+
* trigger's, where `channelId` is the trigger's channel
|
|
517
|
+
* (TurnRouting.triggerOrigin), else the locus's, where it's the channel the
|
|
518
|
+
* locus's server put there (TurnRouting.locusOrigin).
|
|
519
|
+
*/
|
|
520
|
+
function triggerServerFor(routing, channelId) {
|
|
521
|
+
if (routing === undefined || !channelId)
|
|
522
|
+
return undefined;
|
|
523
|
+
if (routing.triggerOrigin?.channelId === channelId)
|
|
524
|
+
return routing.triggerOrigin.serverId;
|
|
525
|
+
if (routing.locusOrigin?.channelId === channelId)
|
|
526
|
+
return routing.locusOrigin.serverId;
|
|
527
|
+
return undefined;
|
|
528
|
+
}
|
|
529
|
+
/** Move a turn's locus, with the server of the message that put it there
|
|
530
|
+
* (TurnRouting.locusOrigin), or none. */
|
|
531
|
+
function setTurnLocus(routing, channelId, serverId) {
|
|
532
|
+
routing.locus = channelId;
|
|
533
|
+
routing.locusOrigin = serverId !== undefined ? { channelId, serverId } : undefined;
|
|
534
|
+
}
|
|
535
|
+
/** routeSpeech's options for speech into `channelId` in `routing`'s turn:
|
|
536
|
+
* the trigger's server, where it's the trigger's channel (triggerServerFor). */
|
|
537
|
+
function speechServerOpts(routing, channelId) {
|
|
538
|
+
const serverId = triggerServerFor(routing, channelId);
|
|
539
|
+
return serverId !== undefined ? { locusServerId: serverId } : undefined;
|
|
540
|
+
}
|
|
494
541
|
function freshTurnRouting() {
|
|
495
542
|
return {
|
|
496
543
|
turnId: ++lastTurnId,
|
|
@@ -1965,8 +2012,8 @@ export class AgentFramework {
|
|
|
1965
2012
|
// route applies to a fork only when it IS that home, and never to
|
|
1966
2013
|
// another agent when a fork owns that channel. Otherwise a fork
|
|
1967
2014
|
// bound elsewhere would show typing where it isn't replying.
|
|
1968
|
-
...(provenance?.routeChannelId && framework.mayTakeGateRoute(agentName, provenance.routeChannelId)
|
|
1969
|
-
? { channelId: provenance.routeChannelId, addressed: true }
|
|
2015
|
+
...(provenance?.routeChannelId && framework.mayTakeGateRoute(agentName, provenance.routeChannelId, provenance.routeServerId)
|
|
2016
|
+
? { channelId: provenance.routeChannelId, serverId: provenance.routeServerId, addressed: true }
|
|
1970
2017
|
: {}),
|
|
1971
2018
|
});
|
|
1972
2019
|
},
|
|
@@ -6933,7 +6980,9 @@ export class AgentFramework {
|
|
|
6933
6980
|
// tools/call directly on its connection (bare tool name — no prefix
|
|
6934
6981
|
// games), bypassing the agent event queue so no synthetic tool-result
|
|
6935
6982
|
// is injected into a turn the agent never took.
|
|
6936
|
-
|
|
6983
|
+
// Through the server whose message it marks: the latest accepted
|
|
6984
|
+
// message's, kept beside its channel and id (fallbackServerFor).
|
|
6985
|
+
const serverId = this.channelRegistry?.fallbackServerFor(incoming.channelId);
|
|
6937
6986
|
const server = serverId ? this.mcplServerRegistry?.getServer(serverId) : null;
|
|
6938
6987
|
if (!server) {
|
|
6939
6988
|
console.error(`[inference-refusal] reaction skipped: no MCPL server for locus ${logValue(incoming.channelId)} (agent=${agentName})`);
|
|
@@ -7090,7 +7139,9 @@ export class AgentFramework {
|
|
|
7090
7139
|
if (parts[0] !== 'discord')
|
|
7091
7140
|
return;
|
|
7092
7141
|
const channelId = parts[parts.length - 1];
|
|
7093
|
-
|
|
7142
|
+
// As reactToRefusal resolves it: through the server whose message the
|
|
7143
|
+
// latest inbound is (fallbackServerFor).
|
|
7144
|
+
const serverId = this.channelRegistry?.fallbackServerFor(incoming.channelId);
|
|
7094
7145
|
const server = serverId ? this.mcplServerRegistry?.getServer(serverId) : null;
|
|
7095
7146
|
if (!server)
|
|
7096
7147
|
return;
|
|
@@ -9188,13 +9239,19 @@ export class AgentFramework {
|
|
|
9188
9239
|
// The last injection that qualifies wins (repinChannel).
|
|
9189
9240
|
const routing = this.routingOf(agent.name);
|
|
9190
9241
|
let newLocus;
|
|
9191
|
-
|
|
9192
|
-
|
|
9242
|
+
let newLocusServer;
|
|
9243
|
+
for (const inj of midTurnInjections) {
|
|
9244
|
+
const channelId = this.repinChannel(inj.metadata, routing);
|
|
9245
|
+
if (channelId) {
|
|
9246
|
+
newLocus = channelId;
|
|
9247
|
+
newLocusServer = messageServerId(inj.metadata);
|
|
9248
|
+
}
|
|
9249
|
+
}
|
|
9193
9250
|
if (newLocus) {
|
|
9194
9251
|
// What this boundary just stored is newer than any arrival the
|
|
9195
9252
|
// turn holds, so the arrivals can't move the locus back.
|
|
9196
9253
|
routing.arrivals = [];
|
|
9197
|
-
const notice = this.repinTurnLocus(agent, routing, newLocus, 'mid-turn addressed');
|
|
9254
|
+
const notice = this.repinTurnLocus(agent, routing, newLocus, 'mid-turn addressed', newLocusServer);
|
|
9198
9255
|
if (notice)
|
|
9199
9256
|
midTurnInjections.push(notice);
|
|
9200
9257
|
}
|
|
@@ -9529,14 +9586,15 @@ export class AgentFramework {
|
|
|
9529
9586
|
const id = target.getContextManager().addMessage('user', event.content, metadata);
|
|
9530
9587
|
// A correction may target an older engagement. Refresh only the
|
|
9531
9588
|
// binding whose agent actually received the message.
|
|
9532
|
-
|
|
9533
|
-
|
|
9589
|
+
const channel = { serverId: event.serverId, channelId: event.channelId };
|
|
9590
|
+
if (this.conversationRouter.getBinding(channel)?.agentName === target.name) {
|
|
9591
|
+
this.conversationRouter.touch(channel);
|
|
9534
9592
|
}
|
|
9535
9593
|
this.emitTrace({ type: 'message:added', messageId: id, source: 'mcpl:channel-incoming' });
|
|
9536
9594
|
if (event.triggerInference) {
|
|
9537
9595
|
this.pendingRequests.push({
|
|
9538
9596
|
agentName: target.name, reason: 'mcpl:channel-incoming', source: event.serverId, timestamp: Date.now(),
|
|
9539
|
-
channelId: event.channelId,
|
|
9597
|
+
channelId: event.channelId, serverId: event.serverId,
|
|
9540
9598
|
counterparty: event.author?.id ? `${event.serverId}:user:${event.author.id}` : undefined,
|
|
9541
9599
|
addressed: isAddressedMessage(event.tags, event.metadata),
|
|
9542
9600
|
...(event.coalescingSubject ? { coalescingSubject: event.coalescingSubject } : {}),
|
|
@@ -9608,8 +9666,10 @@ export class AgentFramework {
|
|
|
9608
9666
|
source: event.serverId,
|
|
9609
9667
|
timestamp: Date.now(),
|
|
9610
9668
|
// Route this turn's auto-published speech back to THIS channel, not
|
|
9611
|
-
// the global most-recent-inbound locus (item-3 redux, trunk agents)
|
|
9669
|
+
// the global most-recent-inbound locus (item-3 redux, trunk agents),
|
|
9670
|
+
// through the server that sent it.
|
|
9612
9671
|
channelId: event.channelId,
|
|
9672
|
+
serverId: event.serverId,
|
|
9613
9673
|
counterparty: event.author?.id ? `${event.serverId}:user:${event.author.id}` : undefined,
|
|
9614
9674
|
// Addressed messages outrank ambient chatter when a batched wake
|
|
9615
9675
|
// picks the turn's frozen speech locus.
|
|
@@ -9672,7 +9732,7 @@ export class AgentFramework {
|
|
|
9672
9732
|
if (!agent) {
|
|
9673
9733
|
// Bound agent vanished (e.g. external reset) — unbind so the next
|
|
9674
9734
|
// qualifying message respawns a fresh fork.
|
|
9675
|
-
router.unbind(event.channelId);
|
|
9735
|
+
router.unbind({ serverId: event.serverId, channelId: event.channelId });
|
|
9676
9736
|
this.emitTrace({
|
|
9677
9737
|
type: 'mcpl:conversation-binding-orphaned',
|
|
9678
9738
|
channelId: event.channelId,
|
|
@@ -9686,7 +9746,7 @@ export class AgentFramework {
|
|
|
9686
9746
|
const trigger = decision.trigger && event.triggerInference !== false;
|
|
9687
9747
|
messageMetadata.triggered = trigger;
|
|
9688
9748
|
const id = agent.getContextManager().addMessage('user', event.content, messageMetadata);
|
|
9689
|
-
router.touch(event.channelId);
|
|
9749
|
+
router.touch({ serverId: event.serverId, channelId: event.channelId });
|
|
9690
9750
|
this.emitTrace({ type: 'message:added', messageId: id, source: 'mcpl:channel-incoming' });
|
|
9691
9751
|
if (trigger) {
|
|
9692
9752
|
this.pendingRequests.push({
|
|
@@ -9697,6 +9757,7 @@ export class AgentFramework {
|
|
|
9697
9757
|
// A fork's home channel wins in routeSpeech regardless, but carry the
|
|
9698
9758
|
// triggering channel too so the trunk/active path stays consistent.
|
|
9699
9759
|
channelId: event.channelId,
|
|
9760
|
+
serverId: event.serverId,
|
|
9700
9761
|
counterparty: event.author?.id ? `${event.serverId}:user:${event.author.id}` : undefined,
|
|
9701
9762
|
addressed: isAddressedMessage(event.tags, event.metadata),
|
|
9702
9763
|
...(event.coalescingSubject ? { coalescingSubject: event.coalescingSubject } : {}),
|
|
@@ -9835,7 +9896,7 @@ export class AgentFramework {
|
|
|
9835
9896
|
const decision = this.conversationRouter.route(facts);
|
|
9836
9897
|
if (decision.kind !== 'spawn')
|
|
9837
9898
|
return decision;
|
|
9838
|
-
return { ...decision, spawned: this.spawnConversationAgent(facts
|
|
9899
|
+
return { ...decision, spawned: this.spawnConversationAgent(facts, decision) };
|
|
9839
9900
|
}
|
|
9840
9901
|
}
|
|
9841
9902
|
/**
|
|
@@ -9850,11 +9911,12 @@ export class AgentFramework {
|
|
|
9850
9911
|
* so the entry it removes is its own. It's removed before anyone waiting
|
|
9851
9912
|
* on it resumes, so a waiter that decides again doesn't find it.
|
|
9852
9913
|
*/
|
|
9853
|
-
spawnConversationAgent(
|
|
9914
|
+
spawnConversationAgent(facts, decision) {
|
|
9915
|
+
const channelId = facts.channelId;
|
|
9854
9916
|
const spawn = (async () => {
|
|
9855
9917
|
this.storeConversationGeneration(channelId, decision.generation);
|
|
9856
9918
|
const agent = await this.createConversationAgent(decision.agentName, channelId);
|
|
9857
|
-
this.conversationRouter.bind(channelId, decision.agentName, decision.generation);
|
|
9919
|
+
this.conversationRouter.bind({ serverId: facts.serverId, channelId }, decision.agentName, decision.generation);
|
|
9858
9920
|
return agent;
|
|
9859
9921
|
})();
|
|
9860
9922
|
const done = () => { this.conversationSpawns.delete(channelId); };
|
|
@@ -9975,7 +10037,7 @@ export class AgentFramework {
|
|
|
9975
10037
|
return;
|
|
9976
10038
|
this.lastConversationSweep = now;
|
|
9977
10039
|
for (const binding of this.conversationRouter.expired(now)) {
|
|
9978
|
-
this.conversationRouter.unbind(binding
|
|
10040
|
+
this.conversationRouter.unbind(binding);
|
|
9979
10041
|
this.emitTrace({
|
|
9980
10042
|
type: 'mcpl:conversation-closed',
|
|
9981
10043
|
channelId: binding.channelId,
|
|
@@ -10612,7 +10674,7 @@ export class AgentFramework {
|
|
|
10612
10674
|
return explicit.filter((n) => this.agents.has(n));
|
|
10613
10675
|
if (occ.scope.kind === 'channel' && this.conversationRouter) {
|
|
10614
10676
|
const router = this.conversationRouter;
|
|
10615
|
-
const bound = router.getBinding(occ.scope.id)?.agentName;
|
|
10677
|
+
const bound = router.getBinding({ serverId: occ.serverId, channelId: occ.scope.id })?.agentName;
|
|
10616
10678
|
if (bound)
|
|
10617
10679
|
return this.agents.has(bound) ? [bound] : [];
|
|
10618
10680
|
// No fork yet: the same bind decision an incoming message would get. A
|
|
@@ -10717,6 +10779,7 @@ export class AgentFramework {
|
|
|
10717
10779
|
source: occ.serverId,
|
|
10718
10780
|
timestamp: Date.now(),
|
|
10719
10781
|
channelId,
|
|
10782
|
+
serverId: channelId !== undefined ? occ.serverId : undefined,
|
|
10720
10783
|
counterparty: authorId ? `${occ.serverId}:user:${authorId}` : undefined,
|
|
10721
10784
|
addressed: isAddressedMessage(occ.tags, origin),
|
|
10722
10785
|
coalescingSubject: subject,
|
|
@@ -10935,6 +10998,7 @@ export class AgentFramework {
|
|
|
10935
10998
|
source: event.serverId,
|
|
10936
10999
|
timestamp: Date.now(),
|
|
10937
11000
|
channelId: triggerChannel?.channelId,
|
|
11001
|
+
serverId: triggerChannel ? event.serverId : undefined,
|
|
10938
11002
|
// DMs and addressed-while-closed messages arrive as push events;
|
|
10939
11003
|
// they must outrank ambient chatter in a batched wake's locus
|
|
10940
11004
|
// selection just like their channels/incoming counterparts.
|
|
@@ -10955,18 +11019,49 @@ export class AgentFramework {
|
|
|
10955
11019
|
* Whether a batched gate wake's addressed route may become `agentName`'s
|
|
10956
11020
|
* speech locus. The gate broadcasts to every agent, conversation forks
|
|
10957
11021
|
* included. A fork speaks only in its home channel, and a channel bound to
|
|
10958
|
-
* a fork is that fork's conversation, so no other agent takes it.
|
|
11022
|
+
* a fork is that fork's conversation, so no other agent takes it. Two
|
|
11023
|
+
* servers' channels with one id are two conversations, each bound to its
|
|
11024
|
+
* own fork with that id as its home, so a fork takes the route only where
|
|
11025
|
+
* the router binds the route's channel, its server and id, to it; a route
|
|
11026
|
+
* with no server names a fork only where one fork is bound to that id.
|
|
10959
11027
|
*/
|
|
10960
|
-
mayTakeGateRoute(agentName, routeChannelId) {
|
|
11028
|
+
mayTakeGateRoute(agentName, routeChannelId, routeServerId) {
|
|
10961
11029
|
const home = this.conversationAgentHomes.get(agentName);
|
|
10962
|
-
if (home !== undefined)
|
|
10963
|
-
return home === routeChannelId
|
|
11030
|
+
if (home !== undefined) {
|
|
11031
|
+
return home === routeChannelId
|
|
11032
|
+
&& this.conversationRouter?.getBinding({ serverId: routeServerId, channelId: routeChannelId })?.agentName === agentName;
|
|
11033
|
+
}
|
|
10964
11034
|
for (const boundChannel of this.conversationAgentHomes.values()) {
|
|
10965
11035
|
if (boundChannel === routeChannelId)
|
|
10966
11036
|
return false;
|
|
10967
11037
|
}
|
|
10968
11038
|
return true;
|
|
10969
11039
|
}
|
|
11040
|
+
/**
|
|
11041
|
+
* The channel a push names, and a server that registers it, when another
|
|
11042
|
+
* server registers that channel id and the pushing server doesn't. A
|
|
11043
|
+
* channel's id is the agent's locus on its own, so the reply to such a
|
|
11044
|
+
* push would go out through the server that registers it: the push
|
|
11045
|
+
* boundary refuses it (PushHandler). A push naming a channel no server
|
|
11046
|
+
* registers isn't refused, and registers it for the pushing server
|
|
11047
|
+
* (ensureChannelRegistered).
|
|
11048
|
+
*/
|
|
11049
|
+
channelOfAnotherServer(serverId, origin) {
|
|
11050
|
+
if (!this.channelRegistry)
|
|
11051
|
+
return undefined;
|
|
11052
|
+
const channel = this.derivePushEventChannel(origin);
|
|
11053
|
+
if (!channel)
|
|
11054
|
+
return undefined;
|
|
11055
|
+
let holder;
|
|
11056
|
+
for (const entry of this.channelRegistry.listChannelsRaw()) {
|
|
11057
|
+
if (entry.descriptor.id !== channel.channelId)
|
|
11058
|
+
continue;
|
|
11059
|
+
if (entry.serverId === serverId)
|
|
11060
|
+
return undefined;
|
|
11061
|
+
holder ??= entry.serverId;
|
|
11062
|
+
}
|
|
11063
|
+
return holder === undefined ? undefined : { channelId: channel.channelId, holder };
|
|
11064
|
+
}
|
|
10970
11065
|
/**
|
|
10971
11066
|
* Derive the MCPL composite channel id (the outbound routing locus) for a
|
|
10972
11067
|
* push event from its server-defined `origin`, if it carries one.
|
|
@@ -11278,6 +11373,7 @@ export class AgentFramework {
|
|
|
11278
11373
|
ephemeralSystemPrompt: silentOnly ? trigger?.ephemeralSystemPrompt : undefined,
|
|
11279
11374
|
silentHeartbeat: silentOnly ? trigger?.silentHeartbeat : undefined,
|
|
11280
11375
|
channelId: channelReq?.channelId,
|
|
11376
|
+
serverId: channelReq?.channelId !== undefined ? onlyServerFor(requests, channelReq.channelId) : undefined,
|
|
11281
11377
|
addressed: addressedReq !== undefined,
|
|
11282
11378
|
// A context-budget restart continues the same logical turn: it keeps
|
|
11283
11379
|
// the channel for routing but names no author — the restart is its
|
|
@@ -11702,8 +11798,8 @@ export class AgentFramework {
|
|
|
11702
11798
|
/** The drafts tool's answer to a resident's call, from the model's own
|
|
11703
11799
|
* dispatch or an agent-origin call through executeToolCall (a puppet's):
|
|
11704
11800
|
* a failure the handler throws is answered as one. */
|
|
11705
|
-
draftsToolResult(agentName, rawInput) {
|
|
11706
|
-
return this.handleDraftsTool(agentName, rawInput).catch((err) => ({
|
|
11801
|
+
draftsToolResult(agentName, rawInput, callerTurnId) {
|
|
11802
|
+
return this.handleDraftsTool(agentName, rawInput, callerTurnId).catch((err) => ({
|
|
11707
11803
|
success: false,
|
|
11708
11804
|
error: `drafts failed: ${thrownText(err)}`,
|
|
11709
11805
|
isError: true,
|
|
@@ -11715,7 +11811,7 @@ export class AgentFramework {
|
|
|
11715
11811
|
* each draft is published verbatim, in order, through the normal publish
|
|
11716
11812
|
* executor, its attempt journaled durably before the request leaves.
|
|
11717
11813
|
*/
|
|
11718
|
-
async handleDraftsTool(agentName, rawInput) {
|
|
11814
|
+
async handleDraftsTool(agentName, rawInput, callerTurnId) {
|
|
11719
11815
|
const input = (rawInput && typeof rawInput === 'object' && !Array.isArray(rawInput) ? rawInput : {});
|
|
11720
11816
|
// The turn that sends: a resent draft's channel is recorded as engaged on
|
|
11721
11817
|
// it, as a send tool's is, even when the result comes after the turn.
|
|
@@ -11886,7 +11982,17 @@ export class AgentFramework {
|
|
|
11886
11982
|
const cand = resolved.candidates?.length ? ` It could mean: ${resolved.candidates.join(', ')}.` : '';
|
|
11887
11983
|
return refuse(`Destination "${spec}" did not resolve: ${resolved.error}.${cand} Nothing was sent.`);
|
|
11888
11984
|
}
|
|
11889
|
-
|
|
11985
|
+
// A resend into a channel the calling turn recorded a server for goes
|
|
11986
|
+
// through that server (triggerServerFor), where the call was made inside
|
|
11987
|
+
// that turn; a background script's call carries no turn.
|
|
11988
|
+
const callingRouting = this.turnRouting.get(agentName);
|
|
11989
|
+
const triggerServer = callerTurnId !== undefined && callingRouting?.turnId === callerTurnId
|
|
11990
|
+
? triggerServerFor(callingRouting, resolved.channelId)
|
|
11991
|
+
: undefined;
|
|
11992
|
+
const target = registry.resolveDestination({
|
|
11993
|
+
...(triggerServer !== undefined ? { serverId: triggerServer } : {}),
|
|
11994
|
+
channelId: resolved.channelId,
|
|
11995
|
+
});
|
|
11890
11996
|
if ('error' in target)
|
|
11891
11997
|
return refuse(`Destination "${spec}": ${target.error}. Nothing was sent.`);
|
|
11892
11998
|
const destination = target.destination;
|
|
@@ -12223,8 +12329,10 @@ export class AgentFramework {
|
|
|
12223
12329
|
if (metadata?.tuneOut)
|
|
12224
12330
|
return;
|
|
12225
12331
|
const channelId = this.repinChannel(metadata, open);
|
|
12226
|
-
if (channelId)
|
|
12227
|
-
|
|
12332
|
+
if (channelId) {
|
|
12333
|
+
const serverId = messageServerId(metadata);
|
|
12334
|
+
open.arrivals.push({ messageId, channelId, ...(serverId !== undefined ? { serverId } : {}) });
|
|
12335
|
+
}
|
|
12228
12336
|
}
|
|
12229
12337
|
/**
|
|
12230
12338
|
* Whether `agent`'s store is the residents' one message slot, which every
|
|
@@ -12261,12 +12369,12 @@ export class AgentFramework {
|
|
|
12261
12369
|
remainingArrival(agent, routing) {
|
|
12262
12370
|
const cm = agent.getContextManager();
|
|
12263
12371
|
for (let i = routing.arrivals.length - 1; i >= 0; i--) {
|
|
12264
|
-
const
|
|
12372
|
+
const arrival = routing.arrivals[i];
|
|
12265
12373
|
// The store answers null for a message that isn't there. A store that
|
|
12266
12374
|
// can't be read throws here, as anywhere else in the start, rather
|
|
12267
12375
|
// than have its candidates read as withdrawn.
|
|
12268
|
-
if (cm.getMessage(messageId))
|
|
12269
|
-
return
|
|
12376
|
+
if (cm.getMessage(arrival.messageId))
|
|
12377
|
+
return arrival;
|
|
12270
12378
|
}
|
|
12271
12379
|
return undefined;
|
|
12272
12380
|
}
|
|
@@ -12282,11 +12390,13 @@ export class AgentFramework {
|
|
|
12282
12390
|
* stored. The locus has moved then too, but the announce baseline hasn't,
|
|
12283
12391
|
* so the agent's next turn announces where its speech lands.
|
|
12284
12392
|
*/
|
|
12285
|
-
repinTurnLocus(agent, routing, channelId, cause
|
|
12393
|
+
repinTurnLocus(agent, routing, channelId, cause,
|
|
12394
|
+
/** The server of the message that moved it. */
|
|
12395
|
+
serverId) {
|
|
12286
12396
|
if (routing.locus === channelId)
|
|
12287
12397
|
return undefined;
|
|
12288
12398
|
const prevPin = routing.locus ?? null;
|
|
12289
|
-
routing
|
|
12399
|
+
setTurnLocus(routing, channelId, serverId);
|
|
12290
12400
|
const label = this.channelRegistry?.getDescriptor(channelId)?.label;
|
|
12291
12401
|
const shown = label && label !== channelId
|
|
12292
12402
|
? `${label.startsWith('#') ? label : `#${label}`} (${channelId})`
|
|
@@ -12512,11 +12622,13 @@ export class AgentFramework {
|
|
|
12512
12622
|
* draft's attempt is journaled durably before the request leaves, and its
|
|
12513
12623
|
* outcome after, exactly as a drafts resend would.
|
|
12514
12624
|
*/
|
|
12515
|
-
async deliverWithUnsent(agent, body, channelId, unsent
|
|
12625
|
+
async deliverWithUnsent(agent, body, channelId, unsent,
|
|
12626
|
+
/** The server the channel resolves with (triggerServerFor). */
|
|
12627
|
+
serverId) {
|
|
12516
12628
|
const registry = this.channelRegistry;
|
|
12517
12629
|
let attemptId;
|
|
12518
12630
|
if (unsent) {
|
|
12519
|
-
const target = registry.resolveDestination({ channelId });
|
|
12631
|
+
const target = registry.resolveDestination({ ...(serverId !== undefined ? { serverId } : {}), channelId });
|
|
12520
12632
|
if (!('error' in target)) {
|
|
12521
12633
|
try {
|
|
12522
12634
|
attemptId = this.proseDrafts.beginAttempt(agent.name, unsent.id, target.destination, 'unsent-token', false);
|
|
@@ -12538,7 +12650,7 @@ export class AgentFramework {
|
|
|
12538
12650
|
try {
|
|
12539
12651
|
// Both callers hold a failed delivery's words as a bounce draft, whose
|
|
12540
12652
|
// notice says what happened, so the registry writes no second marker.
|
|
12541
|
-
outcome = await registry.deliverSpeech(agent.name, body, channelId, { heldOnFailure: true });
|
|
12653
|
+
outcome = await registry.deliverSpeech(agent.name, body, channelId, { heldOnFailure: true, ...(serverId !== undefined ? { locusServerId: serverId } : {}) });
|
|
12542
12654
|
}
|
|
12543
12655
|
finally {
|
|
12544
12656
|
if (flight) {
|
|
@@ -12628,7 +12740,7 @@ export class AgentFramework {
|
|
|
12628
12740
|
continue;
|
|
12629
12741
|
}
|
|
12630
12742
|
try {
|
|
12631
|
-
const outcome = await this.deliverWithUnsent(agent, body, resolved.channelId, unsent.draft);
|
|
12743
|
+
const outcome = await this.deliverWithUnsent(agent, body, resolved.channelId, unsent.draft, triggerServerFor(routing, resolved.channelId));
|
|
12632
12744
|
this.recordProseDelivery(routing, outcome.status === 'delivered' ? { delivered: true, channelId: outcome.destination.channelId } : null);
|
|
12633
12745
|
if (outcome.status === 'delivered') {
|
|
12634
12746
|
this.proseBounceStreaks.delete(agent.name);
|
|
@@ -12672,7 +12784,7 @@ export class AgentFramework {
|
|
|
12672
12784
|
}
|
|
12673
12785
|
const target = sticky ?? locus;
|
|
12674
12786
|
try {
|
|
12675
|
-
const outcome = await this.channelRegistry.routeSpeech(agent.name, envelope, target);
|
|
12787
|
+
const outcome = await this.channelRegistry.routeSpeech(agent.name, envelope, target, speechServerOpts(routing, target));
|
|
12676
12788
|
this.recordProseDelivery(routing, outcome);
|
|
12677
12789
|
}
|
|
12678
12790
|
catch (err) {
|
|
@@ -12791,7 +12903,7 @@ export class AgentFramework {
|
|
|
12791
12903
|
return;
|
|
12792
12904
|
}
|
|
12793
12905
|
try {
|
|
12794
|
-
const outcome = await this.deliverWithUnsent(agent, body, targetChannel, unsent.draft);
|
|
12906
|
+
const outcome = await this.deliverWithUnsent(agent, body, targetChannel, unsent.draft, triggerServerFor(routing, targetChannel));
|
|
12795
12907
|
if (outcome.status === 'delivered') {
|
|
12796
12908
|
this.proseBounceStreaks.delete(name);
|
|
12797
12909
|
}
|
|
@@ -13358,6 +13470,9 @@ export class AgentFramework {
|
|
|
13358
13470
|
// turn, which falls back to the global default rather than inheriting
|
|
13359
13471
|
// a previous turn's channel.
|
|
13360
13472
|
routing.triggerChannel = trigger?.channelId;
|
|
13473
|
+
routing.triggerOrigin = trigger?.channelId && trigger.serverId
|
|
13474
|
+
? { channelId: trigger.channelId, serverId: trigger.serverId }
|
|
13475
|
+
: undefined;
|
|
13361
13476
|
// FREEZE the turn's outbound locus now (turn-frozen routing): the home
|
|
13362
13477
|
// channel for forks, else the trigger channel, else the global default.
|
|
13363
13478
|
// Mid-turn injections must not redirect the agent's plain prose
|
|
@@ -13372,8 +13487,14 @@ export class AgentFramework {
|
|
|
13372
13487
|
// stability).
|
|
13373
13488
|
if (turnProseRouting !== 'explicit' && turnProseRouting !== 'disabled') {
|
|
13374
13489
|
const locus = this.channelRegistry?.resolveLocus(agent.name) ?? null;
|
|
13375
|
-
|
|
13376
|
-
|
|
13490
|
+
// A locus that is neither the trigger's channel nor the agent's home
|
|
13491
|
+
// is the global fallback: its server is the latest accepted
|
|
13492
|
+
// message's, taken now, with the locus.
|
|
13493
|
+
if (locus !== null) {
|
|
13494
|
+
setTurnLocus(routing, locus, locus !== routing.triggerChannel && !this.conversationAgentHomes.has(agent.name)
|
|
13495
|
+
? this.channelRegistry?.fallbackServerFor(locus)
|
|
13496
|
+
: undefined);
|
|
13497
|
+
}
|
|
13377
13498
|
if (attempt === 0)
|
|
13378
13499
|
this.announceLocusIfChanged(agent.name, locus);
|
|
13379
13500
|
}
|
|
@@ -13389,7 +13510,7 @@ export class AgentFramework {
|
|
|
13389
13510
|
: undefined;
|
|
13390
13511
|
routing.arrivals = [];
|
|
13391
13512
|
if (arrival)
|
|
13392
|
-
this.repinTurnLocus(agent, routing, arrival, 'arrival');
|
|
13513
|
+
this.repinTurnLocus(agent, routing, arrival.channelId, 'arrival', arrival.serverId);
|
|
13393
13514
|
}
|
|
13394
13515
|
// A silent start, or a disabled agent's, has no locus to show, whatever a
|
|
13395
13516
|
// channel_open in a silent turn left on its object.
|
|
@@ -13833,7 +13954,7 @@ export class AgentFramework {
|
|
|
13833
13954
|
};
|
|
13834
13955
|
const enqueueSpeech = (text, locus) => {
|
|
13835
13956
|
chainSpeech(async () => {
|
|
13836
|
-
const outcome = await this.channelRegistry.routeSpeech(agent.name, text, locus);
|
|
13957
|
+
const outcome = await this.channelRegistry.routeSpeech(agent.name, text, locus, speechServerOpts(streamRouting, locus));
|
|
13837
13958
|
this.recordProseDelivery(streamRouting, outcome);
|
|
13838
13959
|
}, 'mid-turn speech routing failed:');
|
|
13839
13960
|
};
|
|
@@ -13945,7 +14066,7 @@ export class AgentFramework {
|
|
|
13945
14066
|
: null;
|
|
13946
14067
|
const emitOutgoing = (deltas) => {
|
|
13947
14068
|
for (const rd of deltas) {
|
|
13948
|
-
this.channelRegistry.sendOutgoingChunk(rd.channelId, agent.name, outgoingInferenceId, outgoingIndex++, rd.delta);
|
|
14069
|
+
this.channelRegistry.sendOutgoingChunk(rd.channelId, agent.name, outgoingInferenceId, outgoingIndex++, rd.delta, triggerServerFor(streamRouting, rd.channelId));
|
|
13949
14070
|
}
|
|
13950
14071
|
};
|
|
13951
14072
|
// Tool-result-guard publication boundary. While a SUBMITTED batch is
|
|
@@ -14680,7 +14801,7 @@ export class AgentFramework {
|
|
|
14680
14801
|
console.error(`[routing] ${agent.name}: text-only turn -> routing speech -> ${locus ?? '(none)'}`);
|
|
14681
14802
|
for (const segment of speechSegments) {
|
|
14682
14803
|
try {
|
|
14683
|
-
const outcome = await this.channelRegistry.routeSpeech(agent.name, segment, locus);
|
|
14804
|
+
const outcome = await this.channelRegistry.routeSpeech(agent.name, segment, locus, speechServerOpts(streamRouting, locus));
|
|
14684
14805
|
this.recordProseDelivery(streamRouting, outcome);
|
|
14685
14806
|
}
|
|
14686
14807
|
catch (err) {
|
|
@@ -14774,7 +14895,7 @@ export class AgentFramework {
|
|
|
14774
14895
|
// Deliver sequentially (await each) so the segments land in order.
|
|
14775
14896
|
for (const seg of segments) {
|
|
14776
14897
|
try {
|
|
14777
|
-
const outcome = await this.channelRegistry.routeSpeech(agent.name, seg, locus);
|
|
14898
|
+
const outcome = await this.channelRegistry.routeSpeech(agent.name, seg, locus, speechServerOpts(streamRouting, locus));
|
|
14778
14899
|
this.recordProseDelivery(streamRouting, outcome);
|
|
14779
14900
|
}
|
|
14780
14901
|
catch (err) {
|
|
@@ -15406,7 +15527,7 @@ export class AgentFramework {
|
|
|
15406
15527
|
if (frameReachedTerminal && proseStream) {
|
|
15407
15528
|
emitOutgoing(proseStream.finish());
|
|
15408
15529
|
for (const [channelId, text] of proseStream.byChannel()) {
|
|
15409
|
-
this.channelRegistry.sendOutgoingComplete(channelId, agent.name, outgoingInferenceId, text);
|
|
15530
|
+
this.channelRegistry.sendOutgoingComplete(channelId, agent.name, outgoingInferenceId, text, triggerServerFor(streamRouting, channelId));
|
|
15410
15531
|
}
|
|
15411
15532
|
}
|
|
15412
15533
|
this.frameworkCancelledStreams.delete(`${agent.name}:${myStreamId}`);
|
|
@@ -15669,7 +15790,7 @@ export class AgentFramework {
|
|
|
15669
15790
|
// agent origin and acts as that resident, as the model's own call does
|
|
15670
15791
|
// through dispatchToolCall. A module holds no drafts.
|
|
15671
15792
|
if (call.name === 'drafts' && this.channelRegistry && origin.kind === 'agent') {
|
|
15672
|
-
return this.draftsToolResult(origin.agentName, call.input);
|
|
15793
|
+
return this.draftsToolResult(origin.agentName, call.input, call.callerTurnId);
|
|
15673
15794
|
}
|
|
15674
15795
|
// The utils meta-tool — programmatic callers (code_execution scripts,
|
|
15675
15796
|
// ephemerals, ModuleContext.callTool) reach utilities the same way the
|
|
@@ -17024,7 +17145,7 @@ export class AgentFramework {
|
|
|
17024
17145
|
if (enrichedCall.name === 'drafts' && this.channelRegistry) {
|
|
17025
17146
|
const startedAt = Date.now();
|
|
17026
17147
|
this.emitTrace({ type: 'tool:started', module: 'framework', tool: 'drafts', callId: enrichedCall.id, input: enrichedCall.input });
|
|
17027
|
-
void this.draftsToolResult(agentName, enrichedCall.input)
|
|
17148
|
+
void this.draftsToolResult(agentName, enrichedCall.input, enrichedCall.callerTurnId)
|
|
17028
17149
|
.then((result) => {
|
|
17029
17150
|
this.emitTrace({ type: 'tool:completed', module: 'framework', tool: 'drafts', callId: enrichedCall.id, durationMs: Date.now() - startedAt });
|
|
17030
17151
|
this.pushEvent({ type: 'tool-result', callId: enrichedCall.id, toolName: enrichedCall.name, agentName, moduleName: 'framework', result });
|
|
@@ -18015,7 +18136,7 @@ export class AgentFramework {
|
|
|
18015
18136
|
const source = this.pushSource(event);
|
|
18016
18137
|
this.noteInboundAccepted(source);
|
|
18017
18138
|
return source;
|
|
18018
|
-
});
|
|
18139
|
+
}, (serverId, params) => this.channelOfAnotherServer(serverId, params.origin));
|
|
18019
18140
|
// Server-initiated inference router (Step 6)
|
|
18020
18141
|
this.inferenceRouter = new InferenceRouter(this.membrane, this.hookOrchestrator, this.featureSetManager, inferenceRouting ?? null, (event) => this.emitTrace(event), (serverId, params) => {
|
|
18021
18142
|
const server = this.mcplServerRegistry.getServer(serverId);
|
|
@@ -20125,7 +20246,16 @@ export class AgentFramework {
|
|
|
20125
20246
|
// ordinary wake, starts only once its over-budget boundary has every
|
|
20126
20247
|
// result of the round.
|
|
20127
20248
|
const callingTurn = this.routingOf(agentName);
|
|
20128
|
-
|
|
20249
|
+
const inTurn = call.callerTurnId !== undefined && call.callerTurnId === callingTurn.turnId;
|
|
20250
|
+
this.channelRegistry.handleChannelToolCall(call.name, call.input, {
|
|
20251
|
+
kind: 'agent',
|
|
20252
|
+
agentName,
|
|
20253
|
+
// The turn's recorded servers go only with a call made inside that
|
|
20254
|
+
// turn: a background script's call carries no turn, and takes a bare
|
|
20255
|
+
// id only where exactly one server registers it.
|
|
20256
|
+
...(inTurn && callingTurn.triggerOrigin !== undefined ? { trigger: callingTurn.triggerOrigin } : {}),
|
|
20257
|
+
...(inTurn && callingTurn.locusOrigin !== undefined ? { locus: callingTurn.locusOrigin } : {}),
|
|
20258
|
+
})
|
|
20129
20259
|
.then((result) => {
|
|
20130
20260
|
const durationMs = Date.now() - startTime;
|
|
20131
20261
|
this.emitTrace({
|
|
@@ -20162,7 +20292,13 @@ export class AgentFramework {
|
|
|
20162
20292
|
if (openerAgent
|
|
20163
20293
|
&& (openerAgent.proseRouting === 'locus' || openerAgent.proseRouting === 'hybrid')
|
|
20164
20294
|
&& this.openLogical.get(agentName) === callingTurn) {
|
|
20165
|
-
|
|
20295
|
+
const openedOn = this.channelRegistry.resolveDestination({
|
|
20296
|
+
...(typeof call.input?.serverId === 'string'
|
|
20297
|
+
? { serverId: call.input.serverId }
|
|
20298
|
+
: {}),
|
|
20299
|
+
channelId: opened,
|
|
20300
|
+
});
|
|
20301
|
+
setTurnLocus(callingTurn, opened, 'error' in openedOn ? undefined : openedOn.destination.serverId);
|
|
20166
20302
|
this.lastAnnouncedLocus.set(agentName, opened);
|
|
20167
20303
|
result = {
|
|
20168
20304
|
...result,
|
|
@@ -20415,9 +20551,10 @@ export class AgentFramework {
|
|
|
20415
20551
|
else if (agent?.proseRouting === 'explicit') {
|
|
20416
20552
|
// Explicit mode: announce to the turn's sticky prose target if the
|
|
20417
20553
|
// model has set one; otherwise stay quiet — never guess a channel.
|
|
20418
|
-
const
|
|
20554
|
+
const routing = this.turnRouting.get(agentName);
|
|
20555
|
+
const target = routing?.pin;
|
|
20419
20556
|
if (target) {
|
|
20420
|
-
this.channelRegistry.routeSpeech(agentName, text, target).catch((err) => {
|
|
20557
|
+
this.channelRegistry.routeSpeech(agentName, text, target, speechServerOpts(call.callerTurnId === routing?.turnId ? routing : undefined, target)).catch((err) => {
|
|
20421
20558
|
console.error('[sleep] announce failed:', err instanceof Error ? err.message : err);
|
|
20422
20559
|
});
|
|
20423
20560
|
}
|
|
@@ -20426,8 +20563,9 @@ export class AgentFramework {
|
|
|
20426
20563
|
}
|
|
20427
20564
|
}
|
|
20428
20565
|
else {
|
|
20429
|
-
const
|
|
20430
|
-
|
|
20566
|
+
const routing = this.turnRouting.get(agentName);
|
|
20567
|
+
const locus = routing?.locus ?? null;
|
|
20568
|
+
this.channelRegistry.routeSpeech(agentName, text, locus, speechServerOpts(call.callerTurnId === routing?.turnId ? routing : undefined, locus)).catch((err) => {
|
|
20431
20569
|
console.error('[sleep] announce failed:', err instanceof Error ? err.message : err);
|
|
20432
20570
|
});
|
|
20433
20571
|
}
|