@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.
@@ -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
- const serverId = this.channelRegistry?.getChannelServerId(incoming.channelId);
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
- const serverId = this.channelRegistry?.getChannelServerId(incoming.channelId);
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
- for (const inj of midTurnInjections)
9192
- newLocus = this.repinChannel(inj.metadata, routing) ?? newLocus;
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
- if (this.conversationRouter.getBinding(event.channelId)?.agentName === target.name) {
9533
- this.conversationRouter.touch(event.channelId);
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.channelId, decision) };
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(channelId, decision) {
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.channelId);
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
- const target = registry.resolveDestination({ channelId: resolved.channelId });
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
- open.arrivals.push({ messageId, channelId });
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 { messageId, channelId } = routing.arrivals[i];
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 channelId;
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.locus = channelId;
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
- if (locus !== null)
13376
- routing.locus = locus;
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
- this.channelRegistry.handleChannelToolCall(call.name, call.input, { kind: 'agent', agentName })
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
- callingTurn.locus = opened;
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 target = this.turnRouting.get(agentName)?.pin;
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 locus = this.turnRouting.get(agentName)?.locus ?? null;
20430
- this.channelRegistry.routeSpeech(agentName, text, locus).catch((err) => {
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
  }