@animalabs/agent-framework 0.22.4 → 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 +24 -1
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +249 -62
- 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 +280 -54
- 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
|
@@ -139,6 +139,28 @@ const silenceCauseOf = (calls) => {
|
|
|
139
139
|
return 'private';
|
|
140
140
|
return calls.some(isSilencingCall) ? 'send' : null;
|
|
141
141
|
};
|
|
142
|
+
/**
|
|
143
|
+
* The keys of a stored message's metadata that only the host writes and that
|
|
144
|
+
* some reader takes on its own as what the message is: `system` makes it a
|
|
145
|
+
* host notice, `draftOwner` and `draftIds` one that names those drafts,
|
|
146
|
+
* `tuneOut` a message a tune-out holds from the residents,
|
|
147
|
+
* `coalescingSubject` (beside the occurrence's `eventId`) the delivery of a
|
|
148
|
+
* coalesced occurrence, `deferredWriteId` the landed copy of a deferred
|
|
149
|
+
* write, `external` a module's own row for an outside message,
|
|
150
|
+
* `silentHeartbeat` a row of a silent heartbeat tick, which marks the tick
|
|
151
|
+
* started whoever's row it is (indexSilentTickRows), and
|
|
152
|
+
* `openaiResponsesItems` membrane's mark of an imported Responses history. A
|
|
153
|
+
* server's metadata, a channel message's `metadata` or a push event's
|
|
154
|
+
* `origin`, is stored without them, and the host writes its own after it:
|
|
155
|
+
* MCPL SPEC §16.6 makes metadata the producer's untrusted claims, which a
|
|
156
|
+
* host may disbelieve. Keys a server writes by protocol, such as `tags` and
|
|
157
|
+
* `mentioned`, stay. So does the Discord module's `discordTextFirst`, which
|
|
158
|
+
* it reads only on a row it found by `external`.
|
|
159
|
+
*/
|
|
160
|
+
const HOST_METADATA_KEYS = [
|
|
161
|
+
'system', 'draftOwner', 'draftIds', 'tuneOut', 'coalescingSubject', 'deferredWriteId', 'external',
|
|
162
|
+
'silentHeartbeat', 'openaiResponsesItems',
|
|
163
|
+
];
|
|
142
164
|
/**
|
|
143
165
|
* True when an injected message is real conversational input — something the
|
|
144
166
|
* agent might actually be replying to — rather than ambient machinery.
|
|
@@ -467,8 +489,55 @@ function announcedDomainsOf(announce) {
|
|
|
467
489
|
function resumesLogicalTurn(reason) {
|
|
468
490
|
return reason === 'context_budget_restart' || reason === 'tool_result_guard_retry';
|
|
469
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
|
+
}
|
|
470
497
|
/** The last TurnRouting.turnId given out: ids increase, and are unique within the process. */
|
|
471
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
|
+
}
|
|
472
541
|
function freshTurnRouting() {
|
|
473
542
|
return {
|
|
474
543
|
turnId: ++lastTurnId,
|
|
@@ -1943,8 +2012,8 @@ export class AgentFramework {
|
|
|
1943
2012
|
// route applies to a fork only when it IS that home, and never to
|
|
1944
2013
|
// another agent when a fork owns that channel. Otherwise a fork
|
|
1945
2014
|
// bound elsewhere would show typing where it isn't replying.
|
|
1946
|
-
...(provenance?.routeChannelId && framework.mayTakeGateRoute(agentName, provenance.routeChannelId)
|
|
1947
|
-
? { channelId: provenance.routeChannelId, addressed: true }
|
|
2015
|
+
...(provenance?.routeChannelId && framework.mayTakeGateRoute(agentName, provenance.routeChannelId, provenance.routeServerId)
|
|
2016
|
+
? { channelId: provenance.routeChannelId, serverId: provenance.routeServerId, addressed: true }
|
|
1948
2017
|
: {}),
|
|
1949
2018
|
});
|
|
1950
2019
|
},
|
|
@@ -6911,7 +6980,9 @@ export class AgentFramework {
|
|
|
6911
6980
|
// tools/call directly on its connection (bare tool name — no prefix
|
|
6912
6981
|
// games), bypassing the agent event queue so no synthetic tool-result
|
|
6913
6982
|
// is injected into a turn the agent never took.
|
|
6914
|
-
|
|
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);
|
|
6915
6986
|
const server = serverId ? this.mcplServerRegistry?.getServer(serverId) : null;
|
|
6916
6987
|
if (!server) {
|
|
6917
6988
|
console.error(`[inference-refusal] reaction skipped: no MCPL server for locus ${logValue(incoming.channelId)} (agent=${agentName})`);
|
|
@@ -7068,7 +7139,9 @@ export class AgentFramework {
|
|
|
7068
7139
|
if (parts[0] !== 'discord')
|
|
7069
7140
|
return;
|
|
7070
7141
|
const channelId = parts[parts.length - 1];
|
|
7071
|
-
|
|
7142
|
+
// As reactToRefusal resolves it: through the server whose message the
|
|
7143
|
+
// latest inbound is (fallbackServerFor).
|
|
7144
|
+
const serverId = this.channelRegistry?.fallbackServerFor(incoming.channelId);
|
|
7072
7145
|
const server = serverId ? this.mcplServerRegistry?.getServer(serverId) : null;
|
|
7073
7146
|
if (!server)
|
|
7074
7147
|
return;
|
|
@@ -9012,9 +9085,9 @@ export class AgentFramework {
|
|
|
9012
9085
|
// turn ends. The window stores them here, right after the
|
|
9013
9086
|
// tool_result message, which is exactly where the membrane appends
|
|
9014
9087
|
// them in the live conversation — same participant, same content
|
|
9015
|
-
// blocks,
|
|
9016
|
-
// verbatim, so the next turn's compiled prefix
|
|
9017
|
-
// live request (prompt-cache safe).
|
|
9088
|
+
// blocks, and neither carries the row's metadata — and compile()
|
|
9089
|
+
// emits recent messages verbatim, so the next turn's compiled prefix
|
|
9090
|
+
// byte-matches the live request (prompt-cache safe).
|
|
9018
9091
|
//
|
|
9019
9092
|
// The queue is drained ONLY at the target agent's own boundary:
|
|
9020
9093
|
// draining at another agent's boundary would store the messages
|
|
@@ -9166,13 +9239,19 @@ export class AgentFramework {
|
|
|
9166
9239
|
// The last injection that qualifies wins (repinChannel).
|
|
9167
9240
|
const routing = this.routingOf(agent.name);
|
|
9168
9241
|
let newLocus;
|
|
9169
|
-
|
|
9170
|
-
|
|
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
|
+
}
|
|
9171
9250
|
if (newLocus) {
|
|
9172
9251
|
// What this boundary just stored is newer than any arrival the
|
|
9173
9252
|
// turn holds, so the arrivals can't move the locus back.
|
|
9174
9253
|
routing.arrivals = [];
|
|
9175
|
-
const notice = this.repinTurnLocus(agent, routing, newLocus, 'mid-turn addressed');
|
|
9254
|
+
const notice = this.repinTurnLocus(agent, routing, newLocus, 'mid-turn addressed', newLocusServer);
|
|
9176
9255
|
if (notice)
|
|
9177
9256
|
midTurnInjections.push(notice);
|
|
9178
9257
|
}
|
|
@@ -9254,7 +9333,11 @@ export class AgentFramework {
|
|
|
9254
9333
|
// Mid-turn messages collected above ride along as injected user
|
|
9255
9334
|
// messages (membrane ≥0.5.72) — appended after the tool_result
|
|
9256
9335
|
// envelope so the next round of THIS turn hears them.
|
|
9257
|
-
|
|
9336
|
+
// Each goes as its participant and content alone: a stored row's
|
|
9337
|
+
// metadata is the host's record, not input for membrane.
|
|
9338
|
+
currentState.stream.provideToolResults(agent.toolResultGuard.submissionResults(membraneResults), midTurnInjections.length > 0
|
|
9339
|
+
? { injectedMessages: midTurnInjections.map(({ participant, content }) => ({ participant, content })) }
|
|
9340
|
+
: undefined);
|
|
9258
9341
|
// RFC-006 §3.3: consumption is determined at assembly, and this
|
|
9259
9342
|
// is assembly too when the stream carries the injected messages
|
|
9260
9343
|
// into its next request. Without the mark they would sit above
|
|
@@ -9421,6 +9504,29 @@ export class AgentFramework {
|
|
|
9421
9504
|
// ==========================================================================
|
|
9422
9505
|
// Built-in MCPL event → context message conversion
|
|
9423
9506
|
// ==========================================================================
|
|
9507
|
+
/** The servers and keys whose drop withoutHostKeys has logged, each as
|
|
9508
|
+
* `${serverId}\u0000${key}`. */
|
|
9509
|
+
hostKeysDropped = new Set();
|
|
9510
|
+
/**
|
|
9511
|
+
* A server's metadata as it is stored: a copy without HOST_METADATA_KEYS.
|
|
9512
|
+
* A server may use one of those names for something of its own, so the
|
|
9513
|
+
* first drop of each key from each server is logged.
|
|
9514
|
+
*/
|
|
9515
|
+
withoutHostKeys(serverId, given) {
|
|
9516
|
+
const kept = { ...given };
|
|
9517
|
+
for (const key of HOST_METADATA_KEYS) {
|
|
9518
|
+
if (!Object.hasOwn(kept, key))
|
|
9519
|
+
continue;
|
|
9520
|
+
delete kept[key];
|
|
9521
|
+
const logged = `${serverId}\u0000${key}`;
|
|
9522
|
+
if (this.hostKeysDropped.has(logged))
|
|
9523
|
+
continue;
|
|
9524
|
+
this.hostKeysDropped.add(logged);
|
|
9525
|
+
console.error(`[mcpl] ${serverId}: dropped "${key}" from a message, since only the host writes that key ` +
|
|
9526
|
+
`(logged once for each server and key)`);
|
|
9527
|
+
}
|
|
9528
|
+
return kept;
|
|
9529
|
+
}
|
|
9424
9530
|
/**
|
|
9425
9531
|
* Convert an incoming MCPL channel message to a context message.
|
|
9426
9532
|
* This replaces the old MCPLModule.onProcess() message conversion.
|
|
@@ -9437,7 +9543,7 @@ export class AgentFramework {
|
|
|
9437
9543
|
return undefined;
|
|
9438
9544
|
}
|
|
9439
9545
|
const metadata = {
|
|
9440
|
-
...event.metadata,
|
|
9546
|
+
...this.withoutHostKeys(event.serverId, event.metadata),
|
|
9441
9547
|
...(event.eventId ? { eventId: event.eventId } : {}),
|
|
9442
9548
|
...(event.coalescingSubject ? { coalescingSubject: event.coalescingSubject } : {}),
|
|
9443
9549
|
channelId: event.channelId,
|
|
@@ -9480,14 +9586,15 @@ export class AgentFramework {
|
|
|
9480
9586
|
const id = target.getContextManager().addMessage('user', event.content, metadata);
|
|
9481
9587
|
// A correction may target an older engagement. Refresh only the
|
|
9482
9588
|
// binding whose agent actually received the message.
|
|
9483
|
-
|
|
9484
|
-
|
|
9589
|
+
const channel = { serverId: event.serverId, channelId: event.channelId };
|
|
9590
|
+
if (this.conversationRouter.getBinding(channel)?.agentName === target.name) {
|
|
9591
|
+
this.conversationRouter.touch(channel);
|
|
9485
9592
|
}
|
|
9486
9593
|
this.emitTrace({ type: 'message:added', messageId: id, source: 'mcpl:channel-incoming' });
|
|
9487
9594
|
if (event.triggerInference) {
|
|
9488
9595
|
this.pendingRequests.push({
|
|
9489
9596
|
agentName: target.name, reason: 'mcpl:channel-incoming', source: event.serverId, timestamp: Date.now(),
|
|
9490
|
-
channelId: event.channelId,
|
|
9597
|
+
channelId: event.channelId, serverId: event.serverId,
|
|
9491
9598
|
counterparty: event.author?.id ? `${event.serverId}:user:${event.author.id}` : undefined,
|
|
9492
9599
|
addressed: isAddressedMessage(event.tags, event.metadata),
|
|
9493
9600
|
...(event.coalescingSubject ? { coalescingSubject: event.coalescingSubject } : {}),
|
|
@@ -9559,8 +9666,10 @@ export class AgentFramework {
|
|
|
9559
9666
|
source: event.serverId,
|
|
9560
9667
|
timestamp: Date.now(),
|
|
9561
9668
|
// Route this turn's auto-published speech back to THIS channel, not
|
|
9562
|
-
// 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.
|
|
9563
9671
|
channelId: event.channelId,
|
|
9672
|
+
serverId: event.serverId,
|
|
9564
9673
|
counterparty: event.author?.id ? `${event.serverId}:user:${event.author.id}` : undefined,
|
|
9565
9674
|
// Addressed messages outrank ambient chatter when a batched wake
|
|
9566
9675
|
// picks the turn's frozen speech locus.
|
|
@@ -9623,7 +9732,7 @@ export class AgentFramework {
|
|
|
9623
9732
|
if (!agent) {
|
|
9624
9733
|
// Bound agent vanished (e.g. external reset) — unbind so the next
|
|
9625
9734
|
// qualifying message respawns a fresh fork.
|
|
9626
|
-
router.unbind(event.channelId);
|
|
9735
|
+
router.unbind({ serverId: event.serverId, channelId: event.channelId });
|
|
9627
9736
|
this.emitTrace({
|
|
9628
9737
|
type: 'mcpl:conversation-binding-orphaned',
|
|
9629
9738
|
channelId: event.channelId,
|
|
@@ -9637,7 +9746,7 @@ export class AgentFramework {
|
|
|
9637
9746
|
const trigger = decision.trigger && event.triggerInference !== false;
|
|
9638
9747
|
messageMetadata.triggered = trigger;
|
|
9639
9748
|
const id = agent.getContextManager().addMessage('user', event.content, messageMetadata);
|
|
9640
|
-
router.touch(event.channelId);
|
|
9749
|
+
router.touch({ serverId: event.serverId, channelId: event.channelId });
|
|
9641
9750
|
this.emitTrace({ type: 'message:added', messageId: id, source: 'mcpl:channel-incoming' });
|
|
9642
9751
|
if (trigger) {
|
|
9643
9752
|
this.pendingRequests.push({
|
|
@@ -9648,6 +9757,7 @@ export class AgentFramework {
|
|
|
9648
9757
|
// A fork's home channel wins in routeSpeech regardless, but carry the
|
|
9649
9758
|
// triggering channel too so the trunk/active path stays consistent.
|
|
9650
9759
|
channelId: event.channelId,
|
|
9760
|
+
serverId: event.serverId,
|
|
9651
9761
|
counterparty: event.author?.id ? `${event.serverId}:user:${event.author.id}` : undefined,
|
|
9652
9762
|
addressed: isAddressedMessage(event.tags, event.metadata),
|
|
9653
9763
|
...(event.coalescingSubject ? { coalescingSubject: event.coalescingSubject } : {}),
|
|
@@ -9786,7 +9896,7 @@ export class AgentFramework {
|
|
|
9786
9896
|
const decision = this.conversationRouter.route(facts);
|
|
9787
9897
|
if (decision.kind !== 'spawn')
|
|
9788
9898
|
return decision;
|
|
9789
|
-
return { ...decision, spawned: this.spawnConversationAgent(facts
|
|
9899
|
+
return { ...decision, spawned: this.spawnConversationAgent(facts, decision) };
|
|
9790
9900
|
}
|
|
9791
9901
|
}
|
|
9792
9902
|
/**
|
|
@@ -9801,11 +9911,12 @@ export class AgentFramework {
|
|
|
9801
9911
|
* so the entry it removes is its own. It's removed before anyone waiting
|
|
9802
9912
|
* on it resumes, so a waiter that decides again doesn't find it.
|
|
9803
9913
|
*/
|
|
9804
|
-
spawnConversationAgent(
|
|
9914
|
+
spawnConversationAgent(facts, decision) {
|
|
9915
|
+
const channelId = facts.channelId;
|
|
9805
9916
|
const spawn = (async () => {
|
|
9806
9917
|
this.storeConversationGeneration(channelId, decision.generation);
|
|
9807
9918
|
const agent = await this.createConversationAgent(decision.agentName, channelId);
|
|
9808
|
-
this.conversationRouter.bind(channelId, decision.agentName, decision.generation);
|
|
9919
|
+
this.conversationRouter.bind({ serverId: facts.serverId, channelId }, decision.agentName, decision.generation);
|
|
9809
9920
|
return agent;
|
|
9810
9921
|
})();
|
|
9811
9922
|
const done = () => { this.conversationSpawns.delete(channelId); };
|
|
@@ -9926,7 +10037,7 @@ export class AgentFramework {
|
|
|
9926
10037
|
return;
|
|
9927
10038
|
this.lastConversationSweep = now;
|
|
9928
10039
|
for (const binding of this.conversationRouter.expired(now)) {
|
|
9929
|
-
this.conversationRouter.unbind(binding
|
|
10040
|
+
this.conversationRouter.unbind(binding);
|
|
9930
10041
|
this.emitTrace({
|
|
9931
10042
|
type: 'mcpl:conversation-closed',
|
|
9932
10043
|
channelId: binding.channelId,
|
|
@@ -10563,7 +10674,7 @@ export class AgentFramework {
|
|
|
10563
10674
|
return explicit.filter((n) => this.agents.has(n));
|
|
10564
10675
|
if (occ.scope.kind === 'channel' && this.conversationRouter) {
|
|
10565
10676
|
const router = this.conversationRouter;
|
|
10566
|
-
const bound = router.getBinding(occ.scope.id)?.agentName;
|
|
10677
|
+
const bound = router.getBinding({ serverId: occ.serverId, channelId: occ.scope.id })?.agentName;
|
|
10567
10678
|
if (bound)
|
|
10568
10679
|
return this.agents.has(bound) ? [bound] : [];
|
|
10569
10680
|
// No fork yet: the same bind decision an incoming message would get. A
|
|
@@ -10668,6 +10779,7 @@ export class AgentFramework {
|
|
|
10668
10779
|
source: occ.serverId,
|
|
10669
10780
|
timestamp: Date.now(),
|
|
10670
10781
|
channelId,
|
|
10782
|
+
serverId: channelId !== undefined ? occ.serverId : undefined,
|
|
10671
10783
|
counterparty: authorId ? `${occ.serverId}:user:${authorId}` : undefined,
|
|
10672
10784
|
addressed: isAddressedMessage(occ.tags, origin),
|
|
10673
10785
|
coalescingSubject: subject,
|
|
@@ -10815,7 +10927,7 @@ export class AgentFramework {
|
|
|
10815
10927
|
this.channelRegistry.ensureChannelRegistered(event.serverId, triggerChannel.channelId, triggerChannel.label, triggerChannel.metadata);
|
|
10816
10928
|
}
|
|
10817
10929
|
const metadata = {
|
|
10818
|
-
...event.origin,
|
|
10930
|
+
...this.withoutHostKeys(event.serverId, event.origin),
|
|
10819
10931
|
serverId: event.serverId,
|
|
10820
10932
|
featureSet: event.featureSet,
|
|
10821
10933
|
eventId: event.eventId,
|
|
@@ -10886,6 +10998,7 @@ export class AgentFramework {
|
|
|
10886
10998
|
source: event.serverId,
|
|
10887
10999
|
timestamp: Date.now(),
|
|
10888
11000
|
channelId: triggerChannel?.channelId,
|
|
11001
|
+
serverId: triggerChannel ? event.serverId : undefined,
|
|
10889
11002
|
// DMs and addressed-while-closed messages arrive as push events;
|
|
10890
11003
|
// they must outrank ambient chatter in a batched wake's locus
|
|
10891
11004
|
// selection just like their channels/incoming counterparts.
|
|
@@ -10906,18 +11019,49 @@ export class AgentFramework {
|
|
|
10906
11019
|
* Whether a batched gate wake's addressed route may become `agentName`'s
|
|
10907
11020
|
* speech locus. The gate broadcasts to every agent, conversation forks
|
|
10908
11021
|
* included. A fork speaks only in its home channel, and a channel bound to
|
|
10909
|
-
* 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.
|
|
10910
11027
|
*/
|
|
10911
|
-
mayTakeGateRoute(agentName, routeChannelId) {
|
|
11028
|
+
mayTakeGateRoute(agentName, routeChannelId, routeServerId) {
|
|
10912
11029
|
const home = this.conversationAgentHomes.get(agentName);
|
|
10913
|
-
if (home !== undefined)
|
|
10914
|
-
return home === routeChannelId
|
|
11030
|
+
if (home !== undefined) {
|
|
11031
|
+
return home === routeChannelId
|
|
11032
|
+
&& this.conversationRouter?.getBinding({ serverId: routeServerId, channelId: routeChannelId })?.agentName === agentName;
|
|
11033
|
+
}
|
|
10915
11034
|
for (const boundChannel of this.conversationAgentHomes.values()) {
|
|
10916
11035
|
if (boundChannel === routeChannelId)
|
|
10917
11036
|
return false;
|
|
10918
11037
|
}
|
|
10919
11038
|
return true;
|
|
10920
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
|
+
}
|
|
10921
11065
|
/**
|
|
10922
11066
|
* Derive the MCPL composite channel id (the outbound routing locus) for a
|
|
10923
11067
|
* push event from its server-defined `origin`, if it carries one.
|
|
@@ -11229,6 +11373,7 @@ export class AgentFramework {
|
|
|
11229
11373
|
ephemeralSystemPrompt: silentOnly ? trigger?.ephemeralSystemPrompt : undefined,
|
|
11230
11374
|
silentHeartbeat: silentOnly ? trigger?.silentHeartbeat : undefined,
|
|
11231
11375
|
channelId: channelReq?.channelId,
|
|
11376
|
+
serverId: channelReq?.channelId !== undefined ? onlyServerFor(requests, channelReq.channelId) : undefined,
|
|
11232
11377
|
addressed: addressedReq !== undefined,
|
|
11233
11378
|
// A context-budget restart continues the same logical turn: it keeps
|
|
11234
11379
|
// the channel for routing but names no author — the restart is its
|
|
@@ -11653,8 +11798,8 @@ export class AgentFramework {
|
|
|
11653
11798
|
/** The drafts tool's answer to a resident's call, from the model's own
|
|
11654
11799
|
* dispatch or an agent-origin call through executeToolCall (a puppet's):
|
|
11655
11800
|
* a failure the handler throws is answered as one. */
|
|
11656
|
-
draftsToolResult(agentName, rawInput) {
|
|
11657
|
-
return this.handleDraftsTool(agentName, rawInput).catch((err) => ({
|
|
11801
|
+
draftsToolResult(agentName, rawInput, callerTurnId) {
|
|
11802
|
+
return this.handleDraftsTool(agentName, rawInput, callerTurnId).catch((err) => ({
|
|
11658
11803
|
success: false,
|
|
11659
11804
|
error: `drafts failed: ${thrownText(err)}`,
|
|
11660
11805
|
isError: true,
|
|
@@ -11666,7 +11811,7 @@ export class AgentFramework {
|
|
|
11666
11811
|
* each draft is published verbatim, in order, through the normal publish
|
|
11667
11812
|
* executor, its attempt journaled durably before the request leaves.
|
|
11668
11813
|
*/
|
|
11669
|
-
async handleDraftsTool(agentName, rawInput) {
|
|
11814
|
+
async handleDraftsTool(agentName, rawInput, callerTurnId) {
|
|
11670
11815
|
const input = (rawInput && typeof rawInput === 'object' && !Array.isArray(rawInput) ? rawInput : {});
|
|
11671
11816
|
// The turn that sends: a resent draft's channel is recorded as engaged on
|
|
11672
11817
|
// it, as a send tool's is, even when the result comes after the turn.
|
|
@@ -11837,7 +11982,17 @@ export class AgentFramework {
|
|
|
11837
11982
|
const cand = resolved.candidates?.length ? ` It could mean: ${resolved.candidates.join(', ')}.` : '';
|
|
11838
11983
|
return refuse(`Destination "${spec}" did not resolve: ${resolved.error}.${cand} Nothing was sent.`);
|
|
11839
11984
|
}
|
|
11840
|
-
|
|
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
|
+
});
|
|
11841
11996
|
if ('error' in target)
|
|
11842
11997
|
return refuse(`Destination "${spec}": ${target.error}. Nothing was sent.`);
|
|
11843
11998
|
const destination = target.destination;
|
|
@@ -12174,8 +12329,10 @@ export class AgentFramework {
|
|
|
12174
12329
|
if (metadata?.tuneOut)
|
|
12175
12330
|
return;
|
|
12176
12331
|
const channelId = this.repinChannel(metadata, open);
|
|
12177
|
-
if (channelId)
|
|
12178
|
-
|
|
12332
|
+
if (channelId) {
|
|
12333
|
+
const serverId = messageServerId(metadata);
|
|
12334
|
+
open.arrivals.push({ messageId, channelId, ...(serverId !== undefined ? { serverId } : {}) });
|
|
12335
|
+
}
|
|
12179
12336
|
}
|
|
12180
12337
|
/**
|
|
12181
12338
|
* Whether `agent`'s store is the residents' one message slot, which every
|
|
@@ -12212,12 +12369,12 @@ export class AgentFramework {
|
|
|
12212
12369
|
remainingArrival(agent, routing) {
|
|
12213
12370
|
const cm = agent.getContextManager();
|
|
12214
12371
|
for (let i = routing.arrivals.length - 1; i >= 0; i--) {
|
|
12215
|
-
const
|
|
12372
|
+
const arrival = routing.arrivals[i];
|
|
12216
12373
|
// The store answers null for a message that isn't there. A store that
|
|
12217
12374
|
// can't be read throws here, as anywhere else in the start, rather
|
|
12218
12375
|
// than have its candidates read as withdrawn.
|
|
12219
|
-
if (cm.getMessage(messageId))
|
|
12220
|
-
return
|
|
12376
|
+
if (cm.getMessage(arrival.messageId))
|
|
12377
|
+
return arrival;
|
|
12221
12378
|
}
|
|
12222
12379
|
return undefined;
|
|
12223
12380
|
}
|
|
@@ -12233,11 +12390,13 @@ export class AgentFramework {
|
|
|
12233
12390
|
* stored. The locus has moved then too, but the announce baseline hasn't,
|
|
12234
12391
|
* so the agent's next turn announces where its speech lands.
|
|
12235
12392
|
*/
|
|
12236
|
-
repinTurnLocus(agent, routing, channelId, cause
|
|
12393
|
+
repinTurnLocus(agent, routing, channelId, cause,
|
|
12394
|
+
/** The server of the message that moved it. */
|
|
12395
|
+
serverId) {
|
|
12237
12396
|
if (routing.locus === channelId)
|
|
12238
12397
|
return undefined;
|
|
12239
12398
|
const prevPin = routing.locus ?? null;
|
|
12240
|
-
routing
|
|
12399
|
+
setTurnLocus(routing, channelId, serverId);
|
|
12241
12400
|
const label = this.channelRegistry?.getDescriptor(channelId)?.label;
|
|
12242
12401
|
const shown = label && label !== channelId
|
|
12243
12402
|
? `${label.startsWith('#') ? label : `#${label}`} (${channelId})`
|
|
@@ -12463,11 +12622,13 @@ export class AgentFramework {
|
|
|
12463
12622
|
* draft's attempt is journaled durably before the request leaves, and its
|
|
12464
12623
|
* outcome after, exactly as a drafts resend would.
|
|
12465
12624
|
*/
|
|
12466
|
-
async deliverWithUnsent(agent, body, channelId, unsent
|
|
12625
|
+
async deliverWithUnsent(agent, body, channelId, unsent,
|
|
12626
|
+
/** The server the channel resolves with (triggerServerFor). */
|
|
12627
|
+
serverId) {
|
|
12467
12628
|
const registry = this.channelRegistry;
|
|
12468
12629
|
let attemptId;
|
|
12469
12630
|
if (unsent) {
|
|
12470
|
-
const target = registry.resolveDestination({ channelId });
|
|
12631
|
+
const target = registry.resolveDestination({ ...(serverId !== undefined ? { serverId } : {}), channelId });
|
|
12471
12632
|
if (!('error' in target)) {
|
|
12472
12633
|
try {
|
|
12473
12634
|
attemptId = this.proseDrafts.beginAttempt(agent.name, unsent.id, target.destination, 'unsent-token', false);
|
|
@@ -12489,7 +12650,7 @@ export class AgentFramework {
|
|
|
12489
12650
|
try {
|
|
12490
12651
|
// Both callers hold a failed delivery's words as a bounce draft, whose
|
|
12491
12652
|
// notice says what happened, so the registry writes no second marker.
|
|
12492
|
-
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 } : {}) });
|
|
12493
12654
|
}
|
|
12494
12655
|
finally {
|
|
12495
12656
|
if (flight) {
|
|
@@ -12579,7 +12740,7 @@ export class AgentFramework {
|
|
|
12579
12740
|
continue;
|
|
12580
12741
|
}
|
|
12581
12742
|
try {
|
|
12582
|
-
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));
|
|
12583
12744
|
this.recordProseDelivery(routing, outcome.status === 'delivered' ? { delivered: true, channelId: outcome.destination.channelId } : null);
|
|
12584
12745
|
if (outcome.status === 'delivered') {
|
|
12585
12746
|
this.proseBounceStreaks.delete(agent.name);
|
|
@@ -12623,7 +12784,7 @@ export class AgentFramework {
|
|
|
12623
12784
|
}
|
|
12624
12785
|
const target = sticky ?? locus;
|
|
12625
12786
|
try {
|
|
12626
|
-
const outcome = await this.channelRegistry.routeSpeech(agent.name, envelope, target);
|
|
12787
|
+
const outcome = await this.channelRegistry.routeSpeech(agent.name, envelope, target, speechServerOpts(routing, target));
|
|
12627
12788
|
this.recordProseDelivery(routing, outcome);
|
|
12628
12789
|
}
|
|
12629
12790
|
catch (err) {
|
|
@@ -12742,7 +12903,7 @@ export class AgentFramework {
|
|
|
12742
12903
|
return;
|
|
12743
12904
|
}
|
|
12744
12905
|
try {
|
|
12745
|
-
const outcome = await this.deliverWithUnsent(agent, body, targetChannel, unsent.draft);
|
|
12906
|
+
const outcome = await this.deliverWithUnsent(agent, body, targetChannel, unsent.draft, triggerServerFor(routing, targetChannel));
|
|
12746
12907
|
if (outcome.status === 'delivered') {
|
|
12747
12908
|
this.proseBounceStreaks.delete(name);
|
|
12748
12909
|
}
|
|
@@ -13309,6 +13470,9 @@ export class AgentFramework {
|
|
|
13309
13470
|
// turn, which falls back to the global default rather than inheriting
|
|
13310
13471
|
// a previous turn's channel.
|
|
13311
13472
|
routing.triggerChannel = trigger?.channelId;
|
|
13473
|
+
routing.triggerOrigin = trigger?.channelId && trigger.serverId
|
|
13474
|
+
? { channelId: trigger.channelId, serverId: trigger.serverId }
|
|
13475
|
+
: undefined;
|
|
13312
13476
|
// FREEZE the turn's outbound locus now (turn-frozen routing): the home
|
|
13313
13477
|
// channel for forks, else the trigger channel, else the global default.
|
|
13314
13478
|
// Mid-turn injections must not redirect the agent's plain prose
|
|
@@ -13323,8 +13487,14 @@ export class AgentFramework {
|
|
|
13323
13487
|
// stability).
|
|
13324
13488
|
if (turnProseRouting !== 'explicit' && turnProseRouting !== 'disabled') {
|
|
13325
13489
|
const locus = this.channelRegistry?.resolveLocus(agent.name) ?? null;
|
|
13326
|
-
|
|
13327
|
-
|
|
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
|
+
}
|
|
13328
13498
|
if (attempt === 0)
|
|
13329
13499
|
this.announceLocusIfChanged(agent.name, locus);
|
|
13330
13500
|
}
|
|
@@ -13340,7 +13510,7 @@ export class AgentFramework {
|
|
|
13340
13510
|
: undefined;
|
|
13341
13511
|
routing.arrivals = [];
|
|
13342
13512
|
if (arrival)
|
|
13343
|
-
this.repinTurnLocus(agent, routing, arrival, 'arrival');
|
|
13513
|
+
this.repinTurnLocus(agent, routing, arrival.channelId, 'arrival', arrival.serverId);
|
|
13344
13514
|
}
|
|
13345
13515
|
// A silent start, or a disabled agent's, has no locus to show, whatever a
|
|
13346
13516
|
// channel_open in a silent turn left on its object.
|
|
@@ -13784,7 +13954,7 @@ export class AgentFramework {
|
|
|
13784
13954
|
};
|
|
13785
13955
|
const enqueueSpeech = (text, locus) => {
|
|
13786
13956
|
chainSpeech(async () => {
|
|
13787
|
-
const outcome = await this.channelRegistry.routeSpeech(agent.name, text, locus);
|
|
13957
|
+
const outcome = await this.channelRegistry.routeSpeech(agent.name, text, locus, speechServerOpts(streamRouting, locus));
|
|
13788
13958
|
this.recordProseDelivery(streamRouting, outcome);
|
|
13789
13959
|
}, 'mid-turn speech routing failed:');
|
|
13790
13960
|
};
|
|
@@ -13896,7 +14066,7 @@ export class AgentFramework {
|
|
|
13896
14066
|
: null;
|
|
13897
14067
|
const emitOutgoing = (deltas) => {
|
|
13898
14068
|
for (const rd of deltas) {
|
|
13899
|
-
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));
|
|
13900
14070
|
}
|
|
13901
14071
|
};
|
|
13902
14072
|
// Tool-result-guard publication boundary. While a SUBMITTED batch is
|
|
@@ -14631,7 +14801,7 @@ export class AgentFramework {
|
|
|
14631
14801
|
console.error(`[routing] ${agent.name}: text-only turn -> routing speech -> ${locus ?? '(none)'}`);
|
|
14632
14802
|
for (const segment of speechSegments) {
|
|
14633
14803
|
try {
|
|
14634
|
-
const outcome = await this.channelRegistry.routeSpeech(agent.name, segment, locus);
|
|
14804
|
+
const outcome = await this.channelRegistry.routeSpeech(agent.name, segment, locus, speechServerOpts(streamRouting, locus));
|
|
14635
14805
|
this.recordProseDelivery(streamRouting, outcome);
|
|
14636
14806
|
}
|
|
14637
14807
|
catch (err) {
|
|
@@ -14725,7 +14895,7 @@ export class AgentFramework {
|
|
|
14725
14895
|
// Deliver sequentially (await each) so the segments land in order.
|
|
14726
14896
|
for (const seg of segments) {
|
|
14727
14897
|
try {
|
|
14728
|
-
const outcome = await this.channelRegistry.routeSpeech(agent.name, seg, locus);
|
|
14898
|
+
const outcome = await this.channelRegistry.routeSpeech(agent.name, seg, locus, speechServerOpts(streamRouting, locus));
|
|
14729
14899
|
this.recordProseDelivery(streamRouting, outcome);
|
|
14730
14900
|
}
|
|
14731
14901
|
catch (err) {
|
|
@@ -15357,7 +15527,7 @@ export class AgentFramework {
|
|
|
15357
15527
|
if (frameReachedTerminal && proseStream) {
|
|
15358
15528
|
emitOutgoing(proseStream.finish());
|
|
15359
15529
|
for (const [channelId, text] of proseStream.byChannel()) {
|
|
15360
|
-
this.channelRegistry.sendOutgoingComplete(channelId, agent.name, outgoingInferenceId, text);
|
|
15530
|
+
this.channelRegistry.sendOutgoingComplete(channelId, agent.name, outgoingInferenceId, text, triggerServerFor(streamRouting, channelId));
|
|
15361
15531
|
}
|
|
15362
15532
|
}
|
|
15363
15533
|
this.frameworkCancelledStreams.delete(`${agent.name}:${myStreamId}`);
|
|
@@ -15620,7 +15790,7 @@ export class AgentFramework {
|
|
|
15620
15790
|
// agent origin and acts as that resident, as the model's own call does
|
|
15621
15791
|
// through dispatchToolCall. A module holds no drafts.
|
|
15622
15792
|
if (call.name === 'drafts' && this.channelRegistry && origin.kind === 'agent') {
|
|
15623
|
-
return this.draftsToolResult(origin.agentName, call.input);
|
|
15793
|
+
return this.draftsToolResult(origin.agentName, call.input, call.callerTurnId);
|
|
15624
15794
|
}
|
|
15625
15795
|
// The utils meta-tool — programmatic callers (code_execution scripts,
|
|
15626
15796
|
// ephemerals, ModuleContext.callTool) reach utilities the same way the
|
|
@@ -16975,7 +17145,7 @@ export class AgentFramework {
|
|
|
16975
17145
|
if (enrichedCall.name === 'drafts' && this.channelRegistry) {
|
|
16976
17146
|
const startedAt = Date.now();
|
|
16977
17147
|
this.emitTrace({ type: 'tool:started', module: 'framework', tool: 'drafts', callId: enrichedCall.id, input: enrichedCall.input });
|
|
16978
|
-
void this.draftsToolResult(agentName, enrichedCall.input)
|
|
17148
|
+
void this.draftsToolResult(agentName, enrichedCall.input, enrichedCall.callerTurnId)
|
|
16979
17149
|
.then((result) => {
|
|
16980
17150
|
this.emitTrace({ type: 'tool:completed', module: 'framework', tool: 'drafts', callId: enrichedCall.id, durationMs: Date.now() - startedAt });
|
|
16981
17151
|
this.pushEvent({ type: 'tool-result', callId: enrichedCall.id, toolName: enrichedCall.name, agentName, moduleName: 'framework', result });
|
|
@@ -17966,7 +18136,7 @@ export class AgentFramework {
|
|
|
17966
18136
|
const source = this.pushSource(event);
|
|
17967
18137
|
this.noteInboundAccepted(source);
|
|
17968
18138
|
return source;
|
|
17969
|
-
});
|
|
18139
|
+
}, (serverId, params) => this.channelOfAnotherServer(serverId, params.origin));
|
|
17970
18140
|
// Server-initiated inference router (Step 6)
|
|
17971
18141
|
this.inferenceRouter = new InferenceRouter(this.membrane, this.hookOrchestrator, this.featureSetManager, inferenceRouting ?? null, (event) => this.emitTrace(event), (serverId, params) => {
|
|
17972
18142
|
const server = this.mcplServerRegistry.getServer(serverId);
|
|
@@ -20076,7 +20246,16 @@ export class AgentFramework {
|
|
|
20076
20246
|
// ordinary wake, starts only once its over-budget boundary has every
|
|
20077
20247
|
// result of the round.
|
|
20078
20248
|
const callingTurn = this.routingOf(agentName);
|
|
20079
|
-
|
|
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
|
+
})
|
|
20080
20259
|
.then((result) => {
|
|
20081
20260
|
const durationMs = Date.now() - startTime;
|
|
20082
20261
|
this.emitTrace({
|
|
@@ -20113,7 +20292,13 @@ export class AgentFramework {
|
|
|
20113
20292
|
if (openerAgent
|
|
20114
20293
|
&& (openerAgent.proseRouting === 'locus' || openerAgent.proseRouting === 'hybrid')
|
|
20115
20294
|
&& this.openLogical.get(agentName) === callingTurn) {
|
|
20116
|
-
|
|
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);
|
|
20117
20302
|
this.lastAnnouncedLocus.set(agentName, opened);
|
|
20118
20303
|
result = {
|
|
20119
20304
|
...result,
|
|
@@ -20366,9 +20551,10 @@ export class AgentFramework {
|
|
|
20366
20551
|
else if (agent?.proseRouting === 'explicit') {
|
|
20367
20552
|
// Explicit mode: announce to the turn's sticky prose target if the
|
|
20368
20553
|
// model has set one; otherwise stay quiet — never guess a channel.
|
|
20369
|
-
const
|
|
20554
|
+
const routing = this.turnRouting.get(agentName);
|
|
20555
|
+
const target = routing?.pin;
|
|
20370
20556
|
if (target) {
|
|
20371
|
-
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) => {
|
|
20372
20558
|
console.error('[sleep] announce failed:', err instanceof Error ? err.message : err);
|
|
20373
20559
|
});
|
|
20374
20560
|
}
|
|
@@ -20377,8 +20563,9 @@ export class AgentFramework {
|
|
|
20377
20563
|
}
|
|
20378
20564
|
}
|
|
20379
20565
|
else {
|
|
20380
|
-
const
|
|
20381
|
-
|
|
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) => {
|
|
20382
20569
|
console.error('[sleep] announce failed:', err instanceof Error ? err.message : err);
|
|
20383
20570
|
});
|
|
20384
20571
|
}
|