@animalabs/agent-framework 0.23.1 → 0.23.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/framework.d.ts +24 -1
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +246 -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 +94 -25
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +32 -7
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +52 -15
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts +19 -3
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +48 -3
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/types/agent.d.ts +6 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/framework.ts +277 -54
- package/src/gate/event-gate.ts +6 -1
- package/src/mcpl/channel-registry.ts +106 -24
- package/src/mcpl/conversation-router.ts +65 -14
- package/src/mcpl/push-handler.ts +52 -2
- package/src/types/agent.ts +6 -0
package/dist/src/framework.js
CHANGED
|
@@ -180,6 +180,27 @@ const silenceCauseOf = (calls) => {
|
|
|
180
180
|
return 'private';
|
|
181
181
|
return calls.some(isSilencingCall) ? 'send' : null;
|
|
182
182
|
};
|
|
183
|
+
/**
|
|
184
|
+
* The keys of a stored message's metadata that only the host writes and that
|
|
185
|
+
* some reader takes on its own as what the message is: `system` and `kind`
|
|
186
|
+
* make it a host notice of that kind, `draftOwner` and `draftIds` one that
|
|
187
|
+
* names those drafts, `tuneOut` a message a tune-out holds from the
|
|
188
|
+
* residents, `coalescingSubject` (beside the occurrence's `eventId`) the
|
|
189
|
+
* delivery of a coalesced occurrence, `deferredWriteId` the landed copy of a
|
|
190
|
+
* deferred write, `external` a module's own row for an outside message, and
|
|
191
|
+
* `openaiResponsesItems` membrane's mark of an imported Responses history. A
|
|
192
|
+
* server's metadata, a channel message's `metadata` or a push event's
|
|
193
|
+
* `origin`, is stored without them, and the host writes its own after it:
|
|
194
|
+
* MCPL SPEC §16.6 makes metadata the producer's untrusted claims, which a
|
|
195
|
+
* host may disbelieve. Keys a server writes by protocol, such as `tags` and
|
|
196
|
+
* `mentioned`, stay. So do the keys a reader takes only beside one of these:
|
|
197
|
+
* a failed reply's `reason` and `detail` beside its `kind`, and the Discord
|
|
198
|
+
* module's `discordTextFirst` on a row it found by `external`.
|
|
199
|
+
*/
|
|
200
|
+
const HOST_METADATA_KEYS = [
|
|
201
|
+
'system', 'kind', 'draftOwner', 'draftIds', 'tuneOut', 'coalescingSubject', 'deferredWriteId',
|
|
202
|
+
'external', 'openaiResponsesItems',
|
|
203
|
+
];
|
|
183
204
|
/**
|
|
184
205
|
* True when an injected message is real conversational input — something the
|
|
185
206
|
* agent might actually be replying to — rather than ambient machinery.
|
|
@@ -502,8 +523,55 @@ function announcedDomainsOf(announce) {
|
|
|
502
523
|
function resumesLogicalTurn(reason) {
|
|
503
524
|
return reason === 'context_budget_restart' || reason === 'tool_result_guard_retry';
|
|
504
525
|
}
|
|
526
|
+
/** The server a stored channel message came from, as the host wrote it. */
|
|
527
|
+
function messageServerId(metadata) {
|
|
528
|
+
const serverId = metadata?.serverId;
|
|
529
|
+
return typeof serverId === 'string' && serverId ? serverId : undefined;
|
|
530
|
+
}
|
|
505
531
|
/** The last TurnRouting.turnId given out: ids increase, and are unique within the process. */
|
|
506
532
|
let lastTurnId = 0;
|
|
533
|
+
/**
|
|
534
|
+
* The server every request naming `channelId` came from, or undefined where
|
|
535
|
+
* one names no server or they name more than one (TurnRouting.triggerOrigin).
|
|
536
|
+
*/
|
|
537
|
+
function onlyServerFor(requests, channelId) {
|
|
538
|
+
let serverId;
|
|
539
|
+
for (const r of requests) {
|
|
540
|
+
if (r.channelId !== channelId)
|
|
541
|
+
continue;
|
|
542
|
+
if (!r.serverId || (serverId !== undefined && r.serverId !== serverId))
|
|
543
|
+
return undefined;
|
|
544
|
+
serverId = r.serverId;
|
|
545
|
+
}
|
|
546
|
+
return serverId;
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* The server a send in `routing`'s turn resolves `channelId` with: the
|
|
550
|
+
* trigger's, where `channelId` is the trigger's channel
|
|
551
|
+
* (TurnRouting.triggerOrigin), else the locus's, where it's the channel the
|
|
552
|
+
* locus's server put there (TurnRouting.locusOrigin).
|
|
553
|
+
*/
|
|
554
|
+
function triggerServerFor(routing, channelId) {
|
|
555
|
+
if (routing === undefined || !channelId)
|
|
556
|
+
return undefined;
|
|
557
|
+
if (routing.triggerOrigin?.channelId === channelId)
|
|
558
|
+
return routing.triggerOrigin.serverId;
|
|
559
|
+
if (routing.locusOrigin?.channelId === channelId)
|
|
560
|
+
return routing.locusOrigin.serverId;
|
|
561
|
+
return undefined;
|
|
562
|
+
}
|
|
563
|
+
/** Move a turn's locus, with the server of the message that put it there
|
|
564
|
+
* (TurnRouting.locusOrigin), or none. */
|
|
565
|
+
function setTurnLocus(routing, channelId, serverId) {
|
|
566
|
+
routing.locus = channelId;
|
|
567
|
+
routing.locusOrigin = serverId !== undefined ? { channelId, serverId } : undefined;
|
|
568
|
+
}
|
|
569
|
+
/** routeSpeech's options for speech into `channelId` in `routing`'s turn:
|
|
570
|
+
* the trigger's server, where it's the trigger's channel (triggerServerFor). */
|
|
571
|
+
function speechServerOpts(routing, channelId) {
|
|
572
|
+
const serverId = triggerServerFor(routing, channelId);
|
|
573
|
+
return serverId !== undefined ? { locusServerId: serverId } : undefined;
|
|
574
|
+
}
|
|
507
575
|
function freshTurnRouting() {
|
|
508
576
|
return {
|
|
509
577
|
turnId: ++lastTurnId,
|
|
@@ -2129,8 +2197,8 @@ export class AgentFramework {
|
|
|
2129
2197
|
// route applies to a fork only when it IS that home, and never to
|
|
2130
2198
|
// another agent when a fork owns that channel. Otherwise a fork
|
|
2131
2199
|
// bound elsewhere would show typing where it isn't replying.
|
|
2132
|
-
...(provenance?.routeChannelId && framework.mayTakeGateRoute(agentName, provenance.routeChannelId)
|
|
2133
|
-
? { channelId: provenance.routeChannelId, addressed: true }
|
|
2200
|
+
...(provenance?.routeChannelId && framework.mayTakeGateRoute(agentName, provenance.routeChannelId, provenance.routeServerId)
|
|
2201
|
+
? { channelId: provenance.routeChannelId, serverId: provenance.routeServerId, addressed: true }
|
|
2134
2202
|
: {}),
|
|
2135
2203
|
});
|
|
2136
2204
|
},
|
|
@@ -7430,7 +7498,9 @@ export class AgentFramework {
|
|
|
7430
7498
|
// tools/call directly on its connection (bare tool name — no prefix
|
|
7431
7499
|
// games), bypassing the agent event queue so no synthetic tool-result
|
|
7432
7500
|
// is injected into a turn the agent never took.
|
|
7433
|
-
|
|
7501
|
+
// Through the server whose message it marks: the latest accepted
|
|
7502
|
+
// message's, kept beside its channel and id (fallbackServerFor).
|
|
7503
|
+
const serverId = this.channelRegistry?.fallbackServerFor(incoming.channelId);
|
|
7434
7504
|
const server = serverId ? this.mcplServerRegistry?.getServer(serverId) : null;
|
|
7435
7505
|
if (!server) {
|
|
7436
7506
|
console.error(`[inference-refusal] reaction skipped: no MCPL server for locus ${logValue(incoming.channelId)} (agent=${agentName})`);
|
|
@@ -9495,9 +9565,9 @@ export class AgentFramework {
|
|
|
9495
9565
|
// turn ends. The window stores them here, right after the
|
|
9496
9566
|
// tool_result message, which is exactly where the membrane appends
|
|
9497
9567
|
// them in the live conversation — same participant, same content
|
|
9498
|
-
// blocks,
|
|
9499
|
-
// verbatim, so the next turn's compiled prefix
|
|
9500
|
-
// live request (prompt-cache safe).
|
|
9568
|
+
// blocks, and neither carries the row's metadata — and compile()
|
|
9569
|
+
// emits recent messages verbatim, so the next turn's compiled prefix
|
|
9570
|
+
// byte-matches the live request (prompt-cache safe).
|
|
9501
9571
|
//
|
|
9502
9572
|
// The queue is drained ONLY at the target agent's own boundary:
|
|
9503
9573
|
// draining at another agent's boundary would store the messages
|
|
@@ -9664,13 +9734,19 @@ export class AgentFramework {
|
|
|
9664
9734
|
// The last injection that qualifies wins (repinChannel).
|
|
9665
9735
|
const routing = this.routingOf(agent.name);
|
|
9666
9736
|
let newLocus;
|
|
9667
|
-
|
|
9668
|
-
|
|
9737
|
+
let newLocusServer;
|
|
9738
|
+
for (const inj of midTurnInjections) {
|
|
9739
|
+
const channelId = this.repinChannel(inj.metadata, routing);
|
|
9740
|
+
if (channelId) {
|
|
9741
|
+
newLocus = channelId;
|
|
9742
|
+
newLocusServer = messageServerId(inj.metadata);
|
|
9743
|
+
}
|
|
9744
|
+
}
|
|
9669
9745
|
if (newLocus) {
|
|
9670
9746
|
// What this boundary just stored is newer than any arrival the
|
|
9671
9747
|
// turn holds, so the arrivals can't move the locus back.
|
|
9672
9748
|
routing.arrivals = [];
|
|
9673
|
-
const notice = this.repinTurnLocus(agent, routing, newLocus, 'mid-turn addressed');
|
|
9749
|
+
const notice = this.repinTurnLocus(agent, routing, newLocus, 'mid-turn addressed', newLocusServer);
|
|
9674
9750
|
if (notice)
|
|
9675
9751
|
midTurnInjections.push(notice);
|
|
9676
9752
|
}
|
|
@@ -9750,8 +9826,12 @@ export class AgentFramework {
|
|
|
9750
9826
|
// Streaming path: convert results and resume the stream.
|
|
9751
9827
|
// Mid-turn messages collected above ride along as injected user
|
|
9752
9828
|
// messages (membrane ≥0.5.72) — appended after the tool_result
|
|
9753
|
-
// envelope so the next round of THIS turn hears them.
|
|
9754
|
-
|
|
9829
|
+
// envelope so the next round of THIS turn hears them. Each goes
|
|
9830
|
+
// as its participant and content alone: a stored row's metadata
|
|
9831
|
+
// is the host's record, not input for membrane.
|
|
9832
|
+
currentState.stream.provideToolResults(agent.toolResultGuard.submissionResults(membraneResults), midTurnInjections.length > 0
|
|
9833
|
+
? { injectedMessages: midTurnInjections.map(({ participant, content }) => ({ participant, content })) }
|
|
9834
|
+
: undefined);
|
|
9755
9835
|
// RFC-006 §3.3: consumption is determined at assembly, and this
|
|
9756
9836
|
// is assembly too when the stream carries the injected messages
|
|
9757
9837
|
// into its next request. Without the mark they would sit above
|
|
@@ -9921,6 +10001,29 @@ export class AgentFramework {
|
|
|
9921
10001
|
// ==========================================================================
|
|
9922
10002
|
// Built-in MCPL event → context message conversion
|
|
9923
10003
|
// ==========================================================================
|
|
10004
|
+
/** The servers and keys whose drop withoutHostKeys has logged, each as
|
|
10005
|
+
* `${serverId}\u0000${key}`. */
|
|
10006
|
+
hostKeysDropped = new Set();
|
|
10007
|
+
/**
|
|
10008
|
+
* A server's metadata as it is stored: a copy without HOST_METADATA_KEYS.
|
|
10009
|
+
* A server may use one of those names for something of its own, so the
|
|
10010
|
+
* first drop of each key from each server is logged.
|
|
10011
|
+
*/
|
|
10012
|
+
withoutHostKeys(serverId, given) {
|
|
10013
|
+
const kept = { ...given };
|
|
10014
|
+
for (const key of HOST_METADATA_KEYS) {
|
|
10015
|
+
if (!Object.hasOwn(kept, key))
|
|
10016
|
+
continue;
|
|
10017
|
+
delete kept[key];
|
|
10018
|
+
const logged = `${serverId}\u0000${key}`;
|
|
10019
|
+
if (this.hostKeysDropped.has(logged))
|
|
10020
|
+
continue;
|
|
10021
|
+
this.hostKeysDropped.add(logged);
|
|
10022
|
+
console.error(`[mcpl] ${serverId}: dropped "${key}" from a message, since only the host writes that key ` +
|
|
10023
|
+
`(logged once for each server and key)`);
|
|
10024
|
+
}
|
|
10025
|
+
return kept;
|
|
10026
|
+
}
|
|
9924
10027
|
/**
|
|
9925
10028
|
* Convert an incoming MCPL channel message to a context message.
|
|
9926
10029
|
* This replaces the old MCPLModule.onProcess() message conversion.
|
|
@@ -9937,7 +10040,7 @@ export class AgentFramework {
|
|
|
9937
10040
|
return undefined;
|
|
9938
10041
|
}
|
|
9939
10042
|
const metadata = {
|
|
9940
|
-
...event.metadata,
|
|
10043
|
+
...this.withoutHostKeys(event.serverId, event.metadata),
|
|
9941
10044
|
...(event.eventId ? { eventId: event.eventId } : {}),
|
|
9942
10045
|
...(event.coalescingSubject ? { coalescingSubject: event.coalescingSubject } : {}),
|
|
9943
10046
|
channelId: event.channelId,
|
|
@@ -9980,14 +10083,15 @@ export class AgentFramework {
|
|
|
9980
10083
|
const id = target.getContextManager().addMessage('user', event.content, metadata);
|
|
9981
10084
|
// A correction may target an older engagement. Refresh only the
|
|
9982
10085
|
// binding whose agent actually received the message.
|
|
9983
|
-
|
|
9984
|
-
|
|
10086
|
+
const channel = { serverId: event.serverId, channelId: event.channelId };
|
|
10087
|
+
if (this.conversationRouter.getBinding(channel)?.agentName === target.name) {
|
|
10088
|
+
this.conversationRouter.touch(channel);
|
|
9985
10089
|
}
|
|
9986
10090
|
this.emitTrace({ type: 'message:added', messageId: id, source: 'mcpl:channel-incoming' });
|
|
9987
10091
|
if (event.triggerInference) {
|
|
9988
10092
|
this.pendingRequests.push({
|
|
9989
10093
|
agentName: target.name, reason: 'mcpl:channel-incoming', source: event.serverId, timestamp: Date.now(),
|
|
9990
|
-
channelId: event.channelId,
|
|
10094
|
+
channelId: event.channelId, serverId: event.serverId,
|
|
9991
10095
|
counterparty: event.author?.id ? `${event.serverId}:user:${event.author.id}` : undefined,
|
|
9992
10096
|
addressed: isAddressedMessage(event.tags, event.metadata),
|
|
9993
10097
|
...(event.coalescingSubject ? { coalescingSubject: event.coalescingSubject } : {}),
|
|
@@ -10059,8 +10163,10 @@ export class AgentFramework {
|
|
|
10059
10163
|
source: event.serverId,
|
|
10060
10164
|
timestamp: Date.now(),
|
|
10061
10165
|
// Route this turn's auto-published speech back to THIS channel, not
|
|
10062
|
-
// the global most-recent-inbound locus (item-3 redux, trunk agents)
|
|
10166
|
+
// the global most-recent-inbound locus (item-3 redux, trunk agents),
|
|
10167
|
+
// through the server that sent it.
|
|
10063
10168
|
channelId: event.channelId,
|
|
10169
|
+
serverId: event.serverId,
|
|
10064
10170
|
counterparty: event.author?.id ? `${event.serverId}:user:${event.author.id}` : undefined,
|
|
10065
10171
|
// Addressed messages outrank ambient chatter when a batched wake
|
|
10066
10172
|
// picks the turn's frozen speech locus.
|
|
@@ -10123,7 +10229,7 @@ export class AgentFramework {
|
|
|
10123
10229
|
if (!agent) {
|
|
10124
10230
|
// Bound agent vanished (e.g. external reset) — unbind so the next
|
|
10125
10231
|
// qualifying message respawns a fresh fork.
|
|
10126
|
-
router.unbind(event.channelId);
|
|
10232
|
+
router.unbind({ serverId: event.serverId, channelId: event.channelId });
|
|
10127
10233
|
this.emitTrace({
|
|
10128
10234
|
type: 'mcpl:conversation-binding-orphaned',
|
|
10129
10235
|
channelId: event.channelId,
|
|
@@ -10137,7 +10243,7 @@ export class AgentFramework {
|
|
|
10137
10243
|
const trigger = decision.trigger && event.triggerInference !== false;
|
|
10138
10244
|
messageMetadata.triggered = trigger;
|
|
10139
10245
|
const id = agent.getContextManager().addMessage('user', event.content, messageMetadata);
|
|
10140
|
-
router.touch(event.channelId);
|
|
10246
|
+
router.touch({ serverId: event.serverId, channelId: event.channelId });
|
|
10141
10247
|
this.emitTrace({ type: 'message:added', messageId: id, source: 'mcpl:channel-incoming' });
|
|
10142
10248
|
if (trigger) {
|
|
10143
10249
|
this.pendingRequests.push({
|
|
@@ -10148,6 +10254,7 @@ export class AgentFramework {
|
|
|
10148
10254
|
// A fork's home channel wins in routeSpeech regardless, but carry the
|
|
10149
10255
|
// triggering channel too so the trunk/active path stays consistent.
|
|
10150
10256
|
channelId: event.channelId,
|
|
10257
|
+
serverId: event.serverId,
|
|
10151
10258
|
counterparty: event.author?.id ? `${event.serverId}:user:${event.author.id}` : undefined,
|
|
10152
10259
|
addressed: isAddressedMessage(event.tags, event.metadata),
|
|
10153
10260
|
...(event.coalescingSubject ? { coalescingSubject: event.coalescingSubject } : {}),
|
|
@@ -10286,7 +10393,7 @@ export class AgentFramework {
|
|
|
10286
10393
|
const decision = this.conversationRouter.route(facts);
|
|
10287
10394
|
if (decision.kind !== 'spawn')
|
|
10288
10395
|
return decision;
|
|
10289
|
-
return { ...decision, spawned: this.spawnConversationAgent(facts
|
|
10396
|
+
return { ...decision, spawned: this.spawnConversationAgent(facts, decision) };
|
|
10290
10397
|
}
|
|
10291
10398
|
}
|
|
10292
10399
|
/**
|
|
@@ -10301,11 +10408,12 @@ export class AgentFramework {
|
|
|
10301
10408
|
* so the entry it removes is its own. It's removed before anyone waiting
|
|
10302
10409
|
* on it resumes, so a waiter that decides again doesn't find it.
|
|
10303
10410
|
*/
|
|
10304
|
-
spawnConversationAgent(
|
|
10411
|
+
spawnConversationAgent(facts, decision) {
|
|
10412
|
+
const channelId = facts.channelId;
|
|
10305
10413
|
const spawn = (async () => {
|
|
10306
10414
|
this.storeConversationGeneration(channelId, decision.generation);
|
|
10307
10415
|
const agent = await this.createConversationAgent(decision.agentName, channelId);
|
|
10308
|
-
this.conversationRouter.bind(channelId, decision.agentName, decision.generation);
|
|
10416
|
+
this.conversationRouter.bind({ serverId: facts.serverId, channelId }, decision.agentName, decision.generation);
|
|
10309
10417
|
return agent;
|
|
10310
10418
|
})();
|
|
10311
10419
|
const done = () => { this.conversationSpawns.delete(channelId); };
|
|
@@ -10428,7 +10536,7 @@ export class AgentFramework {
|
|
|
10428
10536
|
return;
|
|
10429
10537
|
this.lastConversationSweep = now;
|
|
10430
10538
|
for (const binding of this.conversationRouter.expired(now)) {
|
|
10431
|
-
this.conversationRouter.unbind(binding
|
|
10539
|
+
this.conversationRouter.unbind(binding);
|
|
10432
10540
|
this.emitTrace({
|
|
10433
10541
|
type: 'mcpl:conversation-closed',
|
|
10434
10542
|
channelId: binding.channelId,
|
|
@@ -11076,7 +11184,7 @@ export class AgentFramework {
|
|
|
11076
11184
|
return explicit.filter((n) => this.agents.has(n));
|
|
11077
11185
|
if (occ.scope.kind === 'channel' && this.conversationRouter) {
|
|
11078
11186
|
const router = this.conversationRouter;
|
|
11079
|
-
const bound = router.getBinding(occ.scope.id)?.agentName;
|
|
11187
|
+
const bound = router.getBinding({ serverId: occ.serverId, channelId: occ.scope.id })?.agentName;
|
|
11080
11188
|
if (bound)
|
|
11081
11189
|
return this.agents.has(bound) ? [bound] : [];
|
|
11082
11190
|
// No fork yet: the same bind decision an incoming message would get. A
|
|
@@ -11188,6 +11296,7 @@ export class AgentFramework {
|
|
|
11188
11296
|
source: occ.serverId,
|
|
11189
11297
|
timestamp: Date.now(),
|
|
11190
11298
|
channelId,
|
|
11299
|
+
serverId: channelId !== undefined ? occ.serverId : undefined,
|
|
11191
11300
|
counterparty: authorId ? `${occ.serverId}:user:${authorId}` : undefined,
|
|
11192
11301
|
addressed: isAddressedMessage(occ.tags, origin),
|
|
11193
11302
|
...(plain ? {} : { coalescingSubject: subject, coalescingBatch: true }),
|
|
@@ -11334,7 +11443,7 @@ export class AgentFramework {
|
|
|
11334
11443
|
this.channelRegistry.ensureChannelRegistered(event.serverId, triggerChannel.channelId, triggerChannel.label, triggerChannel.metadata);
|
|
11335
11444
|
}
|
|
11336
11445
|
const metadata = {
|
|
11337
|
-
...event.origin,
|
|
11446
|
+
...this.withoutHostKeys(event.serverId, event.origin),
|
|
11338
11447
|
serverId: event.serverId,
|
|
11339
11448
|
featureSet: event.featureSet,
|
|
11340
11449
|
eventId: event.eventId,
|
|
@@ -11405,6 +11514,7 @@ export class AgentFramework {
|
|
|
11405
11514
|
source: event.serverId,
|
|
11406
11515
|
timestamp: Date.now(),
|
|
11407
11516
|
channelId: triggerChannel?.channelId,
|
|
11517
|
+
serverId: triggerChannel ? event.serverId : undefined,
|
|
11408
11518
|
// DMs and addressed-while-closed messages arrive as push events;
|
|
11409
11519
|
// they must outrank ambient chatter in a batched wake's locus
|
|
11410
11520
|
// selection just like their channels/incoming counterparts.
|
|
@@ -11428,18 +11538,49 @@ export class AgentFramework {
|
|
|
11428
11538
|
* Whether a batched gate wake's addressed route may become `agentName`'s
|
|
11429
11539
|
* speech locus. The gate broadcasts to every agent, conversation forks
|
|
11430
11540
|
* included. A fork speaks only in its home channel, and a channel bound to
|
|
11431
|
-
* a fork is that fork's conversation, so no other agent takes it.
|
|
11541
|
+
* a fork is that fork's conversation, so no other agent takes it. Two
|
|
11542
|
+
* servers' channels with one id are two conversations, each bound to its
|
|
11543
|
+
* own fork with that id as its home, so a fork takes the route only where
|
|
11544
|
+
* the router binds the route's channel, its server and id, to it; a route
|
|
11545
|
+
* with no server names a fork only where one fork is bound to that id.
|
|
11432
11546
|
*/
|
|
11433
|
-
mayTakeGateRoute(agentName, routeChannelId) {
|
|
11547
|
+
mayTakeGateRoute(agentName, routeChannelId, routeServerId) {
|
|
11434
11548
|
const home = this.conversationAgentHomes.get(agentName);
|
|
11435
|
-
if (home !== undefined)
|
|
11436
|
-
return home === routeChannelId
|
|
11549
|
+
if (home !== undefined) {
|
|
11550
|
+
return home === routeChannelId
|
|
11551
|
+
&& this.conversationRouter?.getBinding({ serverId: routeServerId, channelId: routeChannelId })?.agentName === agentName;
|
|
11552
|
+
}
|
|
11437
11553
|
for (const boundChannel of this.conversationAgentHomes.values()) {
|
|
11438
11554
|
if (boundChannel === routeChannelId)
|
|
11439
11555
|
return false;
|
|
11440
11556
|
}
|
|
11441
11557
|
return true;
|
|
11442
11558
|
}
|
|
11559
|
+
/**
|
|
11560
|
+
* The channel a push names, and a server that registers it, when another
|
|
11561
|
+
* server registers that channel id and the pushing server doesn't. A
|
|
11562
|
+
* channel's id is the agent's locus on its own, so the reply to such a
|
|
11563
|
+
* push would go out through the server that registers it: the push
|
|
11564
|
+
* boundary refuses it (PushHandler). A push naming a channel no server
|
|
11565
|
+
* registers isn't refused, and registers it for the pushing server
|
|
11566
|
+
* (ensureChannelRegistered).
|
|
11567
|
+
*/
|
|
11568
|
+
channelOfAnotherServer(serverId, origin) {
|
|
11569
|
+
if (!this.channelRegistry)
|
|
11570
|
+
return undefined;
|
|
11571
|
+
const channel = this.derivePushEventChannel(origin);
|
|
11572
|
+
if (!channel)
|
|
11573
|
+
return undefined;
|
|
11574
|
+
let holder;
|
|
11575
|
+
for (const entry of this.channelRegistry.listChannelsRaw()) {
|
|
11576
|
+
if (entry.descriptor.id !== channel.channelId)
|
|
11577
|
+
continue;
|
|
11578
|
+
if (entry.serverId === serverId)
|
|
11579
|
+
return undefined;
|
|
11580
|
+
holder ??= entry.serverId;
|
|
11581
|
+
}
|
|
11582
|
+
return holder === undefined ? undefined : { channelId: channel.channelId, holder };
|
|
11583
|
+
}
|
|
11443
11584
|
/**
|
|
11444
11585
|
* Derive the MCPL composite channel id (the outbound routing locus) for a
|
|
11445
11586
|
* push event from its server-defined `origin`, if it carries one.
|
|
@@ -11758,6 +11899,7 @@ export class AgentFramework {
|
|
|
11758
11899
|
suppressProse: silentOnly ? trigger?.suppressProse : undefined,
|
|
11759
11900
|
silentHeartbeat: silentOnly ? trigger?.silentHeartbeat : undefined,
|
|
11760
11901
|
channelId: channelReq?.channelId,
|
|
11902
|
+
serverId: channelReq?.channelId !== undefined ? onlyServerFor(requests, channelReq.channelId) : undefined,
|
|
11761
11903
|
addressed: addressedReq !== undefined,
|
|
11762
11904
|
// A context-budget restart continues the same logical turn: it keeps
|
|
11763
11905
|
// the channel for routing but names no author — the restart is its
|
|
@@ -12207,8 +12349,8 @@ export class AgentFramework {
|
|
|
12207
12349
|
/** The drafts tool's answer to a resident's call, from the model's own
|
|
12208
12350
|
* dispatch or an agent-origin call through executeToolCall (a puppet's):
|
|
12209
12351
|
* a failure the handler throws is answered as one. */
|
|
12210
|
-
draftsToolResult(agentName, rawInput) {
|
|
12211
|
-
return this.handleDraftsTool(agentName, rawInput).catch((err) => ({
|
|
12352
|
+
draftsToolResult(agentName, rawInput, callerTurnId) {
|
|
12353
|
+
return this.handleDraftsTool(agentName, rawInput, callerTurnId).catch((err) => ({
|
|
12212
12354
|
success: false,
|
|
12213
12355
|
error: `drafts failed: ${thrownText(err)}`,
|
|
12214
12356
|
isError: true,
|
|
@@ -12220,7 +12362,7 @@ export class AgentFramework {
|
|
|
12220
12362
|
* each draft is published verbatim, in order, through the normal publish
|
|
12221
12363
|
* executor, its attempt journaled durably before the request leaves.
|
|
12222
12364
|
*/
|
|
12223
|
-
async handleDraftsTool(agentName, rawInput) {
|
|
12365
|
+
async handleDraftsTool(agentName, rawInput, callerTurnId) {
|
|
12224
12366
|
const input = (rawInput && typeof rawInput === 'object' && !Array.isArray(rawInput) ? rawInput : {});
|
|
12225
12367
|
// The turn that sends: a resent draft's channel is recorded as engaged on
|
|
12226
12368
|
// it, as a send tool's is, even when the result comes after the turn.
|
|
@@ -12391,7 +12533,17 @@ export class AgentFramework {
|
|
|
12391
12533
|
const cand = resolved.candidates?.length ? ` It could mean: ${resolved.candidates.join(', ')}.` : '';
|
|
12392
12534
|
return refuse(`Destination "${spec}" did not resolve: ${resolved.error}.${cand} Nothing was sent.`);
|
|
12393
12535
|
}
|
|
12394
|
-
|
|
12536
|
+
// A resend into a channel the calling turn recorded a server for goes
|
|
12537
|
+
// through that server (triggerServerFor), where the call was made inside
|
|
12538
|
+
// that turn; a background script's call carries no turn.
|
|
12539
|
+
const callingRouting = this.turnRouting.get(agentName);
|
|
12540
|
+
const triggerServer = callerTurnId !== undefined && callingRouting?.turnId === callerTurnId
|
|
12541
|
+
? triggerServerFor(callingRouting, resolved.channelId)
|
|
12542
|
+
: undefined;
|
|
12543
|
+
const target = registry.resolveDestination({
|
|
12544
|
+
...(triggerServer !== undefined ? { serverId: triggerServer } : {}),
|
|
12545
|
+
channelId: resolved.channelId,
|
|
12546
|
+
});
|
|
12395
12547
|
if ('error' in target)
|
|
12396
12548
|
return refuse(`Destination "${spec}": ${target.error}. Nothing was sent.`);
|
|
12397
12549
|
const destination = target.destination;
|
|
@@ -12735,8 +12887,10 @@ export class AgentFramework {
|
|
|
12735
12887
|
if (metadata?.tuneOut)
|
|
12736
12888
|
return;
|
|
12737
12889
|
const channelId = this.repinChannel(metadata, open);
|
|
12738
|
-
if (channelId)
|
|
12739
|
-
|
|
12890
|
+
if (channelId) {
|
|
12891
|
+
const serverId = messageServerId(metadata);
|
|
12892
|
+
open.arrivals.push({ messageId, channelId, ...(serverId !== undefined ? { serverId } : {}) });
|
|
12893
|
+
}
|
|
12740
12894
|
}
|
|
12741
12895
|
/**
|
|
12742
12896
|
* Whether `agent`'s store is the residents' one message slot, which every
|
|
@@ -12773,12 +12927,12 @@ export class AgentFramework {
|
|
|
12773
12927
|
remainingArrival(agent, routing) {
|
|
12774
12928
|
const cm = agent.getContextManager();
|
|
12775
12929
|
for (let i = routing.arrivals.length - 1; i >= 0; i--) {
|
|
12776
|
-
const
|
|
12930
|
+
const arrival = routing.arrivals[i];
|
|
12777
12931
|
// The store answers null for a message that isn't there. A store that
|
|
12778
12932
|
// can't be read throws here, as anywhere else in the start, rather
|
|
12779
12933
|
// than have its candidates read as withdrawn.
|
|
12780
|
-
if (cm.getMessage(messageId))
|
|
12781
|
-
return
|
|
12934
|
+
if (cm.getMessage(arrival.messageId))
|
|
12935
|
+
return arrival;
|
|
12782
12936
|
}
|
|
12783
12937
|
return undefined;
|
|
12784
12938
|
}
|
|
@@ -12794,11 +12948,13 @@ export class AgentFramework {
|
|
|
12794
12948
|
* stored. The locus has moved then too, but the announce baseline hasn't,
|
|
12795
12949
|
* so the agent's next turn announces where its speech lands.
|
|
12796
12950
|
*/
|
|
12797
|
-
repinTurnLocus(agent, routing, channelId, cause
|
|
12951
|
+
repinTurnLocus(agent, routing, channelId, cause,
|
|
12952
|
+
/** The server of the message that moved it. */
|
|
12953
|
+
serverId) {
|
|
12798
12954
|
if (routing.locus === channelId)
|
|
12799
12955
|
return undefined;
|
|
12800
12956
|
const prevPin = routing.locus ?? null;
|
|
12801
|
-
routing
|
|
12957
|
+
setTurnLocus(routing, channelId, serverId);
|
|
12802
12958
|
const label = this.channelRegistry?.getDescriptor(channelId)?.label;
|
|
12803
12959
|
const shown = label && label !== channelId
|
|
12804
12960
|
? `${label.startsWith('#') ? label : `#${label}`} (${channelId})`
|
|
@@ -13054,11 +13210,13 @@ export class AgentFramework {
|
|
|
13054
13210
|
* draft's attempt is journaled durably before the request leaves, and its
|
|
13055
13211
|
* outcome after, exactly as a drafts resend would.
|
|
13056
13212
|
*/
|
|
13057
|
-
async deliverWithUnsent(agent, body, channelId, unsent
|
|
13213
|
+
async deliverWithUnsent(agent, body, channelId, unsent,
|
|
13214
|
+
/** The server the channel resolves with (triggerServerFor). */
|
|
13215
|
+
serverId) {
|
|
13058
13216
|
const registry = this.channelRegistry;
|
|
13059
13217
|
let attemptId;
|
|
13060
13218
|
if (unsent) {
|
|
13061
|
-
const target = registry.resolveDestination({ channelId });
|
|
13219
|
+
const target = registry.resolveDestination({ ...(serverId !== undefined ? { serverId } : {}), channelId });
|
|
13062
13220
|
if (!('error' in target)) {
|
|
13063
13221
|
try {
|
|
13064
13222
|
attemptId = this.proseDrafts.beginAttempt(agent.name, unsent.id, target.destination, 'unsent-token', false);
|
|
@@ -13080,7 +13238,7 @@ export class AgentFramework {
|
|
|
13080
13238
|
try {
|
|
13081
13239
|
// Both callers hold a failed delivery's words as a bounce draft, whose
|
|
13082
13240
|
// notice says what happened, so the registry writes no second marker.
|
|
13083
|
-
outcome = await registry.deliverSpeech(agent.name, body, channelId, { heldOnFailure: true });
|
|
13241
|
+
outcome = await registry.deliverSpeech(agent.name, body, channelId, { heldOnFailure: true, ...(serverId !== undefined ? { locusServerId: serverId } : {}) });
|
|
13084
13242
|
}
|
|
13085
13243
|
finally {
|
|
13086
13244
|
if (flight) {
|
|
@@ -13172,7 +13330,7 @@ export class AgentFramework {
|
|
|
13172
13330
|
continue;
|
|
13173
13331
|
}
|
|
13174
13332
|
try {
|
|
13175
|
-
const outcome = await this.deliverWithUnsent(agent, body, resolved.channelId, unsent.draft);
|
|
13333
|
+
const outcome = await this.deliverWithUnsent(agent, body, resolved.channelId, unsent.draft, triggerServerFor(routing, resolved.channelId));
|
|
13176
13334
|
this.recordProseDelivery(routing, outcome.status === 'delivered' ? { delivered: true, channelId: outcome.destination.channelId } : null);
|
|
13177
13335
|
if (outcome.status === 'delivered') {
|
|
13178
13336
|
this.proseBounceStreaks.delete(agent.name);
|
|
@@ -13216,7 +13374,7 @@ export class AgentFramework {
|
|
|
13216
13374
|
}
|
|
13217
13375
|
const target = sticky ?? locus;
|
|
13218
13376
|
try {
|
|
13219
|
-
const outcome = await this.channelRegistry.routeSpeech(agent.name, envelope, target);
|
|
13377
|
+
const outcome = await this.channelRegistry.routeSpeech(agent.name, envelope, target, speechServerOpts(routing, target));
|
|
13220
13378
|
this.recordProseDelivery(routing, outcome);
|
|
13221
13379
|
}
|
|
13222
13380
|
catch (err) {
|
|
@@ -13338,7 +13496,7 @@ export class AgentFramework {
|
|
|
13338
13496
|
return;
|
|
13339
13497
|
}
|
|
13340
13498
|
try {
|
|
13341
|
-
const outcome = await this.deliverWithUnsent(agent, body, targetChannel, unsent.draft);
|
|
13499
|
+
const outcome = await this.deliverWithUnsent(agent, body, targetChannel, unsent.draft, triggerServerFor(routing, targetChannel));
|
|
13342
13500
|
if (outcome.status === 'delivered') {
|
|
13343
13501
|
this.proseBounceStreaks.delete(name);
|
|
13344
13502
|
}
|
|
@@ -14025,6 +14183,9 @@ export class AgentFramework {
|
|
|
14025
14183
|
// turn, which falls back to the global default rather than inheriting
|
|
14026
14184
|
// a previous turn's channel.
|
|
14027
14185
|
routing.triggerChannel = trigger?.channelId;
|
|
14186
|
+
routing.triggerOrigin = trigger?.channelId && trigger.serverId
|
|
14187
|
+
? { channelId: trigger.channelId, serverId: trigger.serverId }
|
|
14188
|
+
: undefined;
|
|
14028
14189
|
// FREEZE the turn's outbound locus now (turn-frozen routing): the home
|
|
14029
14190
|
// channel for forks, else the trigger channel, else the global default,
|
|
14030
14191
|
// which an ephemeral run never takes (takesGlobalFallback).
|
|
@@ -14040,8 +14201,14 @@ export class AgentFramework {
|
|
|
14040
14201
|
// stability).
|
|
14041
14202
|
if (turnProseRouting !== 'explicit' && turnProseRouting !== 'disabled') {
|
|
14042
14203
|
const locus = this.channelRegistry?.resolveLocus(agent.name) ?? null;
|
|
14043
|
-
|
|
14044
|
-
|
|
14204
|
+
// A locus that is neither the trigger's channel nor the agent's home
|
|
14205
|
+
// is the global fallback: its server is the latest accepted
|
|
14206
|
+
// message's, taken now, with the locus.
|
|
14207
|
+
if (locus !== null) {
|
|
14208
|
+
setTurnLocus(routing, locus, locus !== routing.triggerChannel && !this.conversationAgentHomes.has(agent.name)
|
|
14209
|
+
? this.channelRegistry?.fallbackServerFor(locus)
|
|
14210
|
+
: undefined);
|
|
14211
|
+
}
|
|
14045
14212
|
if (attempt === 0)
|
|
14046
14213
|
this.announceLocusIfChanged(agent.name, locus);
|
|
14047
14214
|
}
|
|
@@ -14057,7 +14224,7 @@ export class AgentFramework {
|
|
|
14057
14224
|
: undefined;
|
|
14058
14225
|
routing.arrivals = [];
|
|
14059
14226
|
if (arrival)
|
|
14060
|
-
this.repinTurnLocus(agent, routing, arrival, 'arrival');
|
|
14227
|
+
this.repinTurnLocus(agent, routing, arrival.channelId, 'arrival', arrival.serverId);
|
|
14061
14228
|
}
|
|
14062
14229
|
// A silent start, or a disabled agent's, has no locus to show, whatever a
|
|
14063
14230
|
// channel_open in a silent turn left on its object.
|
|
@@ -14576,7 +14743,7 @@ export class AgentFramework {
|
|
|
14576
14743
|
};
|
|
14577
14744
|
const enqueueSpeech = (text, locus) => {
|
|
14578
14745
|
chainSpeech(async () => {
|
|
14579
|
-
const outcome = await this.channelRegistry.routeSpeech(agent.name, text, locus);
|
|
14746
|
+
const outcome = await this.channelRegistry.routeSpeech(agent.name, text, locus, speechServerOpts(streamRouting, locus));
|
|
14580
14747
|
this.recordProseDelivery(streamRouting, outcome);
|
|
14581
14748
|
}, 'mid-turn speech routing failed:');
|
|
14582
14749
|
};
|
|
@@ -14691,7 +14858,7 @@ export class AgentFramework {
|
|
|
14691
14858
|
: null;
|
|
14692
14859
|
const emitOutgoing = (deltas) => {
|
|
14693
14860
|
for (const rd of deltas) {
|
|
14694
|
-
this.channelRegistry.sendOutgoingChunk(rd.channelId, agent.name, outgoingInferenceId, outgoingIndex++, rd.delta);
|
|
14861
|
+
this.channelRegistry.sendOutgoingChunk(rd.channelId, agent.name, outgoingInferenceId, outgoingIndex++, rd.delta, triggerServerFor(streamRouting, rd.channelId));
|
|
14695
14862
|
}
|
|
14696
14863
|
};
|
|
14697
14864
|
// Tool-result-guard publication boundary. While a SUBMITTED batch is
|
|
@@ -15539,7 +15706,7 @@ export class AgentFramework {
|
|
|
15539
15706
|
console.error(`[routing] ${agent.name}: text-only turn -> routing speech -> ${locus ?? '(none)'}`);
|
|
15540
15707
|
for (const segment of speechSegments) {
|
|
15541
15708
|
try {
|
|
15542
|
-
const outcome = await this.channelRegistry.routeSpeech(agent.name, segment, locus);
|
|
15709
|
+
const outcome = await this.channelRegistry.routeSpeech(agent.name, segment, locus, speechServerOpts(streamRouting, locus));
|
|
15543
15710
|
this.recordProseDelivery(streamRouting, outcome);
|
|
15544
15711
|
}
|
|
15545
15712
|
catch (err) {
|
|
@@ -15633,7 +15800,7 @@ export class AgentFramework {
|
|
|
15633
15800
|
// Deliver sequentially (await each) so the segments land in order.
|
|
15634
15801
|
for (const seg of segments) {
|
|
15635
15802
|
try {
|
|
15636
|
-
const outcome = await this.channelRegistry.routeSpeech(agent.name, seg, locus);
|
|
15803
|
+
const outcome = await this.channelRegistry.routeSpeech(agent.name, seg, locus, speechServerOpts(streamRouting, locus));
|
|
15637
15804
|
this.recordProseDelivery(streamRouting, outcome);
|
|
15638
15805
|
}
|
|
15639
15806
|
catch (err) {
|
|
@@ -16279,7 +16446,7 @@ export class AgentFramework {
|
|
|
16279
16446
|
if (frameReachedTerminal && proseStream) {
|
|
16280
16447
|
emitOutgoing(proseStream.finish());
|
|
16281
16448
|
for (const [channelId, text] of proseStream.byChannel()) {
|
|
16282
|
-
this.channelRegistry.sendOutgoingComplete(channelId, agent.name, outgoingInferenceId, text);
|
|
16449
|
+
this.channelRegistry.sendOutgoingComplete(channelId, agent.name, outgoingInferenceId, text, triggerServerFor(streamRouting, channelId));
|
|
16283
16450
|
}
|
|
16284
16451
|
}
|
|
16285
16452
|
this.frameworkCancelledStreams.delete(`${agent.name}:${myStreamId}`);
|
|
@@ -16554,7 +16721,7 @@ export class AgentFramework {
|
|
|
16554
16721
|
// agent origin and acts as that resident, as the model's own call does
|
|
16555
16722
|
// through dispatchToolCall. A module holds no drafts.
|
|
16556
16723
|
if (call.name === 'drafts' && this.channelRegistry && origin.kind === 'agent') {
|
|
16557
|
-
return this.draftsToolResult(origin.agentName, call.input);
|
|
16724
|
+
return this.draftsToolResult(origin.agentName, call.input, call.callerTurnId);
|
|
16558
16725
|
}
|
|
16559
16726
|
// The utils meta-tool — programmatic callers (code_execution scripts,
|
|
16560
16727
|
// ephemerals, ModuleContext.callTool) reach utilities the same way the
|
|
@@ -18031,7 +18198,7 @@ export class AgentFramework {
|
|
|
18031
18198
|
if (enrichedCall.name === 'drafts' && this.channelRegistry) {
|
|
18032
18199
|
const startedAt = Date.now();
|
|
18033
18200
|
this.emitTrace({ type: 'tool:started', module: 'framework', tool: 'drafts', callId: enrichedCall.id, input: enrichedCall.input });
|
|
18034
|
-
void this.draftsToolResult(agentName, enrichedCall.input)
|
|
18201
|
+
void this.draftsToolResult(agentName, enrichedCall.input, enrichedCall.callerTurnId)
|
|
18035
18202
|
.then((result) => {
|
|
18036
18203
|
this.emitTrace({ type: 'tool:completed', module: 'framework', tool: 'drafts', callId: enrichedCall.id, durationMs: Date.now() - startedAt });
|
|
18037
18204
|
this.pushEvent({ type: 'tool-result', callId: enrichedCall.id, toolName: enrichedCall.name, agentName, moduleName: 'framework', result });
|
|
@@ -19423,7 +19590,7 @@ export class AgentFramework {
|
|
|
19423
19590
|
const source = this.pushSource(event);
|
|
19424
19591
|
this.noteInboundAccepted(source);
|
|
19425
19592
|
return source;
|
|
19426
|
-
});
|
|
19593
|
+
}, (serverId, params) => this.channelOfAnotherServer(serverId, params.origin));
|
|
19427
19594
|
// Server-initiated inference router (Step 6)
|
|
19428
19595
|
this.inferenceRouter = new InferenceRouter(this.membrane, this.hookOrchestrator, this.featureSetManager, inferenceRouting ?? null, (event) => this.emitTrace(event));
|
|
19429
19596
|
// Checkpoint manager (Step 8), built and loaded at the top
|
|
@@ -22688,7 +22855,16 @@ export class AgentFramework {
|
|
|
22688
22855
|
// ordinary wake, starts only once its over-budget boundary has every
|
|
22689
22856
|
// result of the round.
|
|
22690
22857
|
const callingTurn = this.routingOf(agentName);
|
|
22691
|
-
|
|
22858
|
+
const inTurn = call.callerTurnId !== undefined && call.callerTurnId === callingTurn.turnId;
|
|
22859
|
+
this.channelRegistry.handleChannelToolCall(call.name, call.input, {
|
|
22860
|
+
kind: 'agent',
|
|
22861
|
+
agentName,
|
|
22862
|
+
// The turn's recorded servers go only with a call made inside that
|
|
22863
|
+
// turn: a background script's call carries no turn, and takes a bare
|
|
22864
|
+
// id only where exactly one server registers it.
|
|
22865
|
+
...(inTurn && callingTurn.triggerOrigin !== undefined ? { trigger: callingTurn.triggerOrigin } : {}),
|
|
22866
|
+
...(inTurn && callingTurn.locusOrigin !== undefined ? { locus: callingTurn.locusOrigin } : {}),
|
|
22867
|
+
})
|
|
22692
22868
|
.then((result) => {
|
|
22693
22869
|
const durationMs = Date.now() - startTime;
|
|
22694
22870
|
this.emitTrace({
|
|
@@ -22725,7 +22901,13 @@ export class AgentFramework {
|
|
|
22725
22901
|
if (openerAgent
|
|
22726
22902
|
&& (openerAgent.proseRouting === 'locus' || openerAgent.proseRouting === 'hybrid')
|
|
22727
22903
|
&& this.openLogical.get(agentName) === callingTurn) {
|
|
22728
|
-
|
|
22904
|
+
const openedOn = this.channelRegistry.resolveDestination({
|
|
22905
|
+
...(typeof call.input?.serverId === 'string'
|
|
22906
|
+
? { serverId: call.input.serverId }
|
|
22907
|
+
: {}),
|
|
22908
|
+
channelId: opened,
|
|
22909
|
+
});
|
|
22910
|
+
setTurnLocus(callingTurn, opened, 'error' in openedOn ? undefined : openedOn.destination.serverId);
|
|
22729
22911
|
this.lastAnnouncedLocus.set(agentName, opened);
|
|
22730
22912
|
result = {
|
|
22731
22913
|
...result,
|
|
@@ -22978,9 +23160,10 @@ export class AgentFramework {
|
|
|
22978
23160
|
else if (agent?.proseRouting === 'explicit') {
|
|
22979
23161
|
// Explicit mode: announce to the turn's sticky prose target if the
|
|
22980
23162
|
// model has set one; otherwise stay quiet — never guess a channel.
|
|
22981
|
-
const
|
|
23163
|
+
const routing = this.turnRouting.get(agentName);
|
|
23164
|
+
const target = routing?.pin;
|
|
22982
23165
|
if (target) {
|
|
22983
|
-
this.channelRegistry.routeSpeech(agentName, text, target).catch((err) => {
|
|
23166
|
+
this.channelRegistry.routeSpeech(agentName, text, target, speechServerOpts(call.callerTurnId === routing?.turnId ? routing : undefined, target)).catch((err) => {
|
|
22984
23167
|
console.error('[sleep] announce failed:', err instanceof Error ? err.message : err);
|
|
22985
23168
|
});
|
|
22986
23169
|
}
|
|
@@ -22989,8 +23172,9 @@ export class AgentFramework {
|
|
|
22989
23172
|
}
|
|
22990
23173
|
}
|
|
22991
23174
|
else {
|
|
22992
|
-
const
|
|
22993
|
-
|
|
23175
|
+
const routing = this.turnRouting.get(agentName);
|
|
23176
|
+
const locus = routing?.locus ?? null;
|
|
23177
|
+
this.channelRegistry.routeSpeech(agentName, text, locus, speechServerOpts(call.callerTurnId === routing?.turnId ? routing : undefined, locus)).catch((err) => {
|
|
22994
23178
|
console.error('[sleep] announce failed:', err instanceof Error ? err.message : err);
|
|
22995
23179
|
});
|
|
22996
23180
|
}
|