@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/src/framework.ts
CHANGED
|
@@ -238,6 +238,29 @@ const silenceCauseOf = (calls: Array<{ name: string; input?: unknown }>): 'send'
|
|
|
238
238
|
return calls.some(isSilencingCall) ? 'send' : null;
|
|
239
239
|
};
|
|
240
240
|
|
|
241
|
+
/**
|
|
242
|
+
* The keys of a stored message's metadata that only the host writes and that
|
|
243
|
+
* some reader takes on its own as what the message is: `system` makes it a
|
|
244
|
+
* host notice, `draftOwner` and `draftIds` one that names those drafts,
|
|
245
|
+
* `tuneOut` a message a tune-out holds from the residents,
|
|
246
|
+
* `coalescingSubject` (beside the occurrence's `eventId`) the delivery of a
|
|
247
|
+
* coalesced occurrence, `deferredWriteId` the landed copy of a deferred
|
|
248
|
+
* write, `external` a module's own row for an outside message,
|
|
249
|
+
* `silentHeartbeat` a row of a silent heartbeat tick, which marks the tick
|
|
250
|
+
* started whoever's row it is (indexSilentTickRows), and
|
|
251
|
+
* `openaiResponsesItems` membrane's mark of an imported Responses history. A
|
|
252
|
+
* server's metadata, a channel message's `metadata` or a push event's
|
|
253
|
+
* `origin`, is stored without them, and the host writes its own after it:
|
|
254
|
+
* MCPL SPEC §16.6 makes metadata the producer's untrusted claims, which a
|
|
255
|
+
* host may disbelieve. Keys a server writes by protocol, such as `tags` and
|
|
256
|
+
* `mentioned`, stay. So does the Discord module's `discordTextFirst`, which
|
|
257
|
+
* it reads only on a row it found by `external`.
|
|
258
|
+
*/
|
|
259
|
+
const HOST_METADATA_KEYS = [
|
|
260
|
+
'system', 'draftOwner', 'draftIds', 'tuneOut', 'coalescingSubject', 'deferredWriteId', 'external',
|
|
261
|
+
'silentHeartbeat', 'openaiResponsesItems',
|
|
262
|
+
] as const;
|
|
263
|
+
|
|
241
264
|
/**
|
|
242
265
|
* True when an injected message is real conversational input — something the
|
|
243
266
|
* agent might actually be replying to — rather than ambient machinery.
|
|
@@ -731,6 +754,31 @@ interface TurnRouting {
|
|
|
731
754
|
* sent Sol's DM reply to a stale guild channel (2026-07-22).
|
|
732
755
|
*/
|
|
733
756
|
triggerChannel?: string;
|
|
757
|
+
/**
|
|
758
|
+
* The trigger's channel with the server whose message it was, frozen when
|
|
759
|
+
* the turn opens and never moved: a channel_open moves triggerChannel and
|
|
760
|
+
* a re-pin moves the locus, but neither carries this server with it. Every
|
|
761
|
+
* send of the turn whose destination id is this channel resolves with this
|
|
762
|
+
* server (triggerServerFor): speech, a channel_publish naming it or taking
|
|
763
|
+
* it as its default, a drafts resend, and the refusal reaction. So a
|
|
764
|
+
* message one server sent can't have its reply go out through another
|
|
765
|
+
* server that registers the same id, whatever the registry holds by then;
|
|
766
|
+
* the reply is refused where its server no longer registers the channel.
|
|
767
|
+
* Unset for a turn with no channel trigger, and where the requests batched
|
|
768
|
+
* into the trigger name its channel from more than one server: a bare id
|
|
769
|
+
* then resolves only where exactly one server registers it.
|
|
770
|
+
*/
|
|
771
|
+
triggerOrigin?: { channelId: string; serverId: string };
|
|
772
|
+
/**
|
|
773
|
+
* The locus with the server of the message that put it there, where that
|
|
774
|
+
* isn't the trigger: the latest accepted message's (the global fallback a
|
|
775
|
+
* turn with no trigger channel takes), an arrival's or a mid-turn
|
|
776
|
+
* message's (a re-pin), or the server a channel_open opened it on. Set
|
|
777
|
+
* with the locus (setTurnLocus), and cleared when the locus moves to a
|
|
778
|
+
* channel no one server put there. A send into this channel resolves with
|
|
779
|
+
* this server, as one into the trigger's channel does with the trigger's.
|
|
780
|
+
*/
|
|
781
|
+
locusOrigin?: { channelId: string; serverId: string };
|
|
734
782
|
/**
|
|
735
783
|
* Locus and hybrid mode: where the turn's unpinned plain prose lands.
|
|
736
784
|
* Frozen when the turn opens (home channel for a fork, else the trigger
|
|
@@ -807,11 +855,60 @@ interface Arrival {
|
|
|
807
855
|
messageId: MessageId;
|
|
808
856
|
/** The channel it re-pins the locus to. */
|
|
809
857
|
channelId: string;
|
|
858
|
+
/** The server whose message it is. */
|
|
859
|
+
serverId?: string;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/** The server a stored channel message came from, as the host wrote it. */
|
|
863
|
+
function messageServerId(metadata: MessageMetadata | undefined): string | undefined {
|
|
864
|
+
const serverId = (metadata as { serverId?: unknown } | undefined)?.serverId;
|
|
865
|
+
return typeof serverId === 'string' && serverId ? serverId : undefined;
|
|
810
866
|
}
|
|
811
867
|
|
|
812
868
|
/** The last TurnRouting.turnId given out: ids increase, and are unique within the process. */
|
|
813
869
|
let lastTurnId = 0;
|
|
814
870
|
|
|
871
|
+
/**
|
|
872
|
+
* The server every request naming `channelId` came from, or undefined where
|
|
873
|
+
* one names no server or they name more than one (TurnRouting.triggerOrigin).
|
|
874
|
+
*/
|
|
875
|
+
function onlyServerFor(requests: readonly InferenceRequest[], channelId: string): string | undefined {
|
|
876
|
+
let serverId: string | undefined;
|
|
877
|
+
for (const r of requests) {
|
|
878
|
+
if (r.channelId !== channelId) continue;
|
|
879
|
+
if (!r.serverId || (serverId !== undefined && r.serverId !== serverId)) return undefined;
|
|
880
|
+
serverId = r.serverId;
|
|
881
|
+
}
|
|
882
|
+
return serverId;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* The server a send in `routing`'s turn resolves `channelId` with: the
|
|
887
|
+
* trigger's, where `channelId` is the trigger's channel
|
|
888
|
+
* (TurnRouting.triggerOrigin), else the locus's, where it's the channel the
|
|
889
|
+
* locus's server put there (TurnRouting.locusOrigin).
|
|
890
|
+
*/
|
|
891
|
+
function triggerServerFor(routing: TurnRouting | undefined, channelId: string | null | undefined): string | undefined {
|
|
892
|
+
if (routing === undefined || !channelId) return undefined;
|
|
893
|
+
if (routing.triggerOrigin?.channelId === channelId) return routing.triggerOrigin.serverId;
|
|
894
|
+
if (routing.locusOrigin?.channelId === channelId) return routing.locusOrigin.serverId;
|
|
895
|
+
return undefined;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/** Move a turn's locus, with the server of the message that put it there
|
|
899
|
+
* (TurnRouting.locusOrigin), or none. */
|
|
900
|
+
function setTurnLocus(routing: TurnRouting, channelId: string, serverId: string | undefined): void {
|
|
901
|
+
routing.locus = channelId;
|
|
902
|
+
routing.locusOrigin = serverId !== undefined ? { channelId, serverId } : undefined;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/** routeSpeech's options for speech into `channelId` in `routing`'s turn:
|
|
906
|
+
* the trigger's server, where it's the trigger's channel (triggerServerFor). */
|
|
907
|
+
function speechServerOpts(routing: TurnRouting | undefined, channelId: string | null | undefined): { locusServerId: string } | undefined {
|
|
908
|
+
const serverId = triggerServerFor(routing, channelId);
|
|
909
|
+
return serverId !== undefined ? { locusServerId: serverId } : undefined;
|
|
910
|
+
}
|
|
911
|
+
|
|
815
912
|
function freshTurnRouting(): TurnRouting {
|
|
816
913
|
return {
|
|
817
914
|
turnId: ++lastTurnId,
|
|
@@ -2704,8 +2801,8 @@ export class AgentFramework {
|
|
|
2704
2801
|
// route applies to a fork only when it IS that home, and never to
|
|
2705
2802
|
// another agent when a fork owns that channel. Otherwise a fork
|
|
2706
2803
|
// bound elsewhere would show typing where it isn't replying.
|
|
2707
|
-
...(provenance?.routeChannelId && framework.mayTakeGateRoute(agentName, provenance.routeChannelId)
|
|
2708
|
-
? { channelId: provenance.routeChannelId, addressed: true }
|
|
2804
|
+
...(provenance?.routeChannelId && framework.mayTakeGateRoute(agentName, provenance.routeChannelId, provenance.routeServerId)
|
|
2805
|
+
? { channelId: provenance.routeChannelId, serverId: provenance.routeServerId, addressed: true }
|
|
2709
2806
|
: {}),
|
|
2710
2807
|
});
|
|
2711
2808
|
},
|
|
@@ -7937,7 +8034,9 @@ export class AgentFramework {
|
|
|
7937
8034
|
// tools/call directly on its connection (bare tool name — no prefix
|
|
7938
8035
|
// games), bypassing the agent event queue so no synthetic tool-result
|
|
7939
8036
|
// is injected into a turn the agent never took.
|
|
7940
|
-
|
|
8037
|
+
// Through the server whose message it marks: the latest accepted
|
|
8038
|
+
// message's, kept beside its channel and id (fallbackServerFor).
|
|
8039
|
+
const serverId = this.channelRegistry?.fallbackServerFor(incoming.channelId);
|
|
7941
8040
|
const server = serverId ? this.mcplServerRegistry?.getServer(serverId) : null;
|
|
7942
8041
|
if (!server) {
|
|
7943
8042
|
console.error(
|
|
@@ -8110,7 +8209,9 @@ export class AgentFramework {
|
|
|
8110
8209
|
const parts = incoming.channelId.split(':');
|
|
8111
8210
|
if (parts[0] !== 'discord') return;
|
|
8112
8211
|
const channelId = parts[parts.length - 1];
|
|
8113
|
-
|
|
8212
|
+
// As reactToRefusal resolves it: through the server whose message the
|
|
8213
|
+
// latest inbound is (fallbackServerFor).
|
|
8214
|
+
const serverId = this.channelRegistry?.fallbackServerFor(incoming.channelId);
|
|
8114
8215
|
const server = serverId ? this.mcplServerRegistry?.getServer(serverId) : null;
|
|
8115
8216
|
if (!server) return;
|
|
8116
8217
|
await server.sendToolsCall('send_message', {
|
|
@@ -10345,9 +10446,9 @@ export class AgentFramework {
|
|
|
10345
10446
|
// turn ends. The window stores them here, right after the
|
|
10346
10447
|
// tool_result message, which is exactly where the membrane appends
|
|
10347
10448
|
// them in the live conversation — same participant, same content
|
|
10348
|
-
// blocks,
|
|
10349
|
-
// verbatim, so the next turn's compiled prefix
|
|
10350
|
-
// live request (prompt-cache safe).
|
|
10449
|
+
// blocks, and neither carries the row's metadata — and compile()
|
|
10450
|
+
// emits recent messages verbatim, so the next turn's compiled prefix
|
|
10451
|
+
// byte-matches the live request (prompt-cache safe).
|
|
10351
10452
|
//
|
|
10352
10453
|
// The queue is drained ONLY at the target agent's own boundary:
|
|
10353
10454
|
// draining at another agent's boundary would store the messages
|
|
@@ -10506,12 +10607,19 @@ export class AgentFramework {
|
|
|
10506
10607
|
// The last injection that qualifies wins (repinChannel).
|
|
10507
10608
|
const routing = this.routingOf(agent.name);
|
|
10508
10609
|
let newLocus: string | undefined;
|
|
10509
|
-
|
|
10610
|
+
let newLocusServer: string | undefined;
|
|
10611
|
+
for (const inj of midTurnInjections) {
|
|
10612
|
+
const channelId = this.repinChannel(inj.metadata, routing);
|
|
10613
|
+
if (channelId) {
|
|
10614
|
+
newLocus = channelId;
|
|
10615
|
+
newLocusServer = messageServerId(inj.metadata);
|
|
10616
|
+
}
|
|
10617
|
+
}
|
|
10510
10618
|
if (newLocus) {
|
|
10511
10619
|
// What this boundary just stored is newer than any arrival the
|
|
10512
10620
|
// turn holds, so the arrivals can't move the locus back.
|
|
10513
10621
|
routing.arrivals = [];
|
|
10514
|
-
const notice = this.repinTurnLocus(agent, routing, newLocus, 'mid-turn addressed');
|
|
10622
|
+
const notice = this.repinTurnLocus(agent, routing, newLocus, 'mid-turn addressed', newLocusServer);
|
|
10515
10623
|
if (notice) midTurnInjections.push(notice);
|
|
10516
10624
|
}
|
|
10517
10625
|
} else if (routesLocus && !shouldEndTurn) {
|
|
@@ -10589,9 +10697,13 @@ export class AgentFramework {
|
|
|
10589
10697
|
// Mid-turn messages collected above ride along as injected user
|
|
10590
10698
|
// messages (membrane ≥0.5.72) — appended after the tool_result
|
|
10591
10699
|
// envelope so the next round of THIS turn hears them.
|
|
10700
|
+
// Each goes as its participant and content alone: a stored row's
|
|
10701
|
+
// metadata is the host's record, not input for membrane.
|
|
10592
10702
|
currentState.stream.provideToolResults(
|
|
10593
10703
|
agent.toolResultGuard.submissionResults(membraneResults),
|
|
10594
|
-
midTurnInjections.length > 0
|
|
10704
|
+
midTurnInjections.length > 0
|
|
10705
|
+
? { injectedMessages: midTurnInjections.map(({ participant, content }) => ({ participant, content })) }
|
|
10706
|
+
: undefined,
|
|
10595
10707
|
);
|
|
10596
10708
|
// RFC-006 §3.3: consumption is determined at assembly, and this
|
|
10597
10709
|
// is assembly too when the stream carries the injected messages
|
|
@@ -10789,6 +10901,31 @@ export class AgentFramework {
|
|
|
10789
10901
|
// Built-in MCPL event → context message conversion
|
|
10790
10902
|
// ==========================================================================
|
|
10791
10903
|
|
|
10904
|
+
/** The servers and keys whose drop withoutHostKeys has logged, each as
|
|
10905
|
+
* `${serverId}\u0000${key}`. */
|
|
10906
|
+
private readonly hostKeysDropped = new Set<string>();
|
|
10907
|
+
|
|
10908
|
+
/**
|
|
10909
|
+
* A server's metadata as it is stored: a copy without HOST_METADATA_KEYS.
|
|
10910
|
+
* A server may use one of those names for something of its own, so the
|
|
10911
|
+
* first drop of each key from each server is logged.
|
|
10912
|
+
*/
|
|
10913
|
+
private withoutHostKeys(serverId: string, given: Record<string, unknown> | undefined): Record<string, unknown> {
|
|
10914
|
+
const kept: Record<string, unknown> = { ...given };
|
|
10915
|
+
for (const key of HOST_METADATA_KEYS) {
|
|
10916
|
+
if (!Object.hasOwn(kept, key)) continue;
|
|
10917
|
+
delete kept[key];
|
|
10918
|
+
const logged = `${serverId}\u0000${key}`;
|
|
10919
|
+
if (this.hostKeysDropped.has(logged)) continue;
|
|
10920
|
+
this.hostKeysDropped.add(logged);
|
|
10921
|
+
console.error(
|
|
10922
|
+
`[mcpl] ${serverId}: dropped "${key}" from a message, since only the host writes that key ` +
|
|
10923
|
+
`(logged once for each server and key)`,
|
|
10924
|
+
);
|
|
10925
|
+
}
|
|
10926
|
+
return kept;
|
|
10927
|
+
}
|
|
10928
|
+
|
|
10792
10929
|
/**
|
|
10793
10930
|
* Convert an incoming MCPL channel message to a context message.
|
|
10794
10931
|
* This replaces the old MCPLModule.onProcess() message conversion.
|
|
@@ -10825,7 +10962,7 @@ export class AgentFramework {
|
|
|
10825
10962
|
return undefined;
|
|
10826
10963
|
}
|
|
10827
10964
|
const metadata: Record<string, unknown> = {
|
|
10828
|
-
...event.metadata,
|
|
10965
|
+
...this.withoutHostKeys(event.serverId, event.metadata),
|
|
10829
10966
|
...(event.eventId ? { eventId: event.eventId } : {}),
|
|
10830
10967
|
...(event.coalescingSubject ? { coalescingSubject: event.coalescingSubject } : {}),
|
|
10831
10968
|
channelId: event.channelId,
|
|
@@ -10865,14 +11002,15 @@ export class AgentFramework {
|
|
|
10865
11002
|
const id = target.getContextManager().addMessage('user', event.content, metadata);
|
|
10866
11003
|
// A correction may target an older engagement. Refresh only the
|
|
10867
11004
|
// binding whose agent actually received the message.
|
|
10868
|
-
|
|
10869
|
-
|
|
11005
|
+
const channel = { serverId: event.serverId, channelId: event.channelId };
|
|
11006
|
+
if (this.conversationRouter.getBinding(channel)?.agentName === target.name) {
|
|
11007
|
+
this.conversationRouter.touch(channel);
|
|
10870
11008
|
}
|
|
10871
11009
|
this.emitTrace({ type: 'message:added', messageId: id, source: 'mcpl:channel-incoming' });
|
|
10872
11010
|
if (event.triggerInference) {
|
|
10873
11011
|
this.pendingRequests.push({
|
|
10874
11012
|
agentName: target.name, reason: 'mcpl:channel-incoming', source: event.serverId, timestamp: Date.now(),
|
|
10875
|
-
channelId: event.channelId,
|
|
11013
|
+
channelId: event.channelId, serverId: event.serverId,
|
|
10876
11014
|
counterparty: event.author?.id ? `${event.serverId}:user:${event.author.id}` : undefined,
|
|
10877
11015
|
addressed: isAddressedMessage(event.tags, event.metadata),
|
|
10878
11016
|
...(event.coalescingSubject ? { coalescingSubject: event.coalescingSubject } : {}),
|
|
@@ -10951,8 +11089,10 @@ export class AgentFramework {
|
|
|
10951
11089
|
source: event.serverId,
|
|
10952
11090
|
timestamp: Date.now(),
|
|
10953
11091
|
// Route this turn's auto-published speech back to THIS channel, not
|
|
10954
|
-
// the global most-recent-inbound locus (item-3 redux, trunk agents)
|
|
11092
|
+
// the global most-recent-inbound locus (item-3 redux, trunk agents),
|
|
11093
|
+
// through the server that sent it.
|
|
10955
11094
|
channelId: event.channelId,
|
|
11095
|
+
serverId: event.serverId,
|
|
10956
11096
|
counterparty: event.author?.id ? `${event.serverId}:user:${event.author.id}` : undefined,
|
|
10957
11097
|
// Addressed messages outrank ambient chatter when a batched wake
|
|
10958
11098
|
// picks the turn's frozen speech locus.
|
|
@@ -11030,7 +11170,7 @@ export class AgentFramework {
|
|
|
11030
11170
|
if (!agent) {
|
|
11031
11171
|
// Bound agent vanished (e.g. external reset) — unbind so the next
|
|
11032
11172
|
// qualifying message respawns a fresh fork.
|
|
11033
|
-
router.unbind(event.channelId);
|
|
11173
|
+
router.unbind({ serverId: event.serverId, channelId: event.channelId });
|
|
11034
11174
|
this.emitTrace({
|
|
11035
11175
|
type: 'mcpl:conversation-binding-orphaned',
|
|
11036
11176
|
channelId: event.channelId,
|
|
@@ -11046,7 +11186,7 @@ export class AgentFramework {
|
|
|
11046
11186
|
messageMetadata.triggered = trigger;
|
|
11047
11187
|
|
|
11048
11188
|
const id = agent.getContextManager().addMessage('user', event.content, messageMetadata);
|
|
11049
|
-
router.touch(event.channelId);
|
|
11189
|
+
router.touch({ serverId: event.serverId, channelId: event.channelId });
|
|
11050
11190
|
this.emitTrace({ type: 'message:added', messageId: id, source: 'mcpl:channel-incoming' });
|
|
11051
11191
|
|
|
11052
11192
|
if (trigger) {
|
|
@@ -11058,6 +11198,7 @@ export class AgentFramework {
|
|
|
11058
11198
|
// A fork's home channel wins in routeSpeech regardless, but carry the
|
|
11059
11199
|
// triggering channel too so the trunk/active path stays consistent.
|
|
11060
11200
|
channelId: event.channelId,
|
|
11201
|
+
serverId: event.serverId,
|
|
11061
11202
|
counterparty: event.author?.id ? `${event.serverId}:user:${event.author.id}` : undefined,
|
|
11062
11203
|
addressed: isAddressedMessage(event.tags, event.metadata),
|
|
11063
11204
|
...(event.coalescingSubject ? { coalescingSubject: event.coalescingSubject } : {}),
|
|
@@ -11202,7 +11343,7 @@ export class AgentFramework {
|
|
|
11202
11343
|
}
|
|
11203
11344
|
const decision = this.conversationRouter!.route(facts);
|
|
11204
11345
|
if (decision.kind !== 'spawn') return decision;
|
|
11205
|
-
return { ...decision, spawned: this.spawnConversationAgent(facts
|
|
11346
|
+
return { ...decision, spawned: this.spawnConversationAgent(facts, decision) };
|
|
11206
11347
|
}
|
|
11207
11348
|
}
|
|
11208
11349
|
|
|
@@ -11218,11 +11359,12 @@ export class AgentFramework {
|
|
|
11218
11359
|
* so the entry it removes is its own. It's removed before anyone waiting
|
|
11219
11360
|
* on it resumes, so a waiter that decides again doesn't find it.
|
|
11220
11361
|
*/
|
|
11221
|
-
private spawnConversationAgent(
|
|
11362
|
+
private spawnConversationAgent(facts: IncomingMessageFacts, decision: { agentName: string; generation: number }): Promise<Agent> {
|
|
11363
|
+
const channelId = facts.channelId;
|
|
11222
11364
|
const spawn = (async () => {
|
|
11223
11365
|
this.storeConversationGeneration(channelId, decision.generation);
|
|
11224
11366
|
const agent = await this.createConversationAgent(decision.agentName, channelId);
|
|
11225
|
-
this.conversationRouter!.bind(channelId, decision.agentName, decision.generation);
|
|
11367
|
+
this.conversationRouter!.bind({ serverId: facts.serverId, channelId }, decision.agentName, decision.generation);
|
|
11226
11368
|
return agent;
|
|
11227
11369
|
})();
|
|
11228
11370
|
const done = (): void => { this.conversationSpawns.delete(channelId); };
|
|
@@ -11351,7 +11493,7 @@ export class AgentFramework {
|
|
|
11351
11493
|
this.lastConversationSweep = now;
|
|
11352
11494
|
|
|
11353
11495
|
for (const binding of this.conversationRouter.expired(now)) {
|
|
11354
|
-
this.conversationRouter.unbind(binding
|
|
11496
|
+
this.conversationRouter.unbind(binding);
|
|
11355
11497
|
this.emitTrace({
|
|
11356
11498
|
type: 'mcpl:conversation-closed',
|
|
11357
11499
|
channelId: binding.channelId,
|
|
@@ -12020,7 +12162,7 @@ export class AgentFramework {
|
|
|
12020
12162
|
if (explicit) return explicit.filter((n) => this.agents.has(n));
|
|
12021
12163
|
if (occ.scope.kind === 'channel' && this.conversationRouter) {
|
|
12022
12164
|
const router = this.conversationRouter;
|
|
12023
|
-
const bound = router.getBinding(occ.scope.id)?.agentName;
|
|
12165
|
+
const bound = router.getBinding({ serverId: occ.serverId, channelId: occ.scope.id })?.agentName;
|
|
12024
12166
|
if (bound) return this.agents.has(bound) ? [bound] : [];
|
|
12025
12167
|
// No fork yet: the same bind decision an incoming message would get. A
|
|
12026
12168
|
// batch that qualifies spawns its fork now so it has someone to wake.
|
|
@@ -12128,6 +12270,7 @@ export class AgentFramework {
|
|
|
12128
12270
|
source: occ.serverId,
|
|
12129
12271
|
timestamp: Date.now(),
|
|
12130
12272
|
channelId,
|
|
12273
|
+
serverId: channelId !== undefined ? occ.serverId : undefined,
|
|
12131
12274
|
counterparty: authorId ? `${occ.serverId}:user:${authorId}` : undefined,
|
|
12132
12275
|
addressed: isAddressedMessage(occ.tags, origin),
|
|
12133
12276
|
coalescingSubject: subject,
|
|
@@ -12267,7 +12410,7 @@ export class AgentFramework {
|
|
|
12267
12410
|
}
|
|
12268
12411
|
|
|
12269
12412
|
const metadata: Record<string, unknown> = {
|
|
12270
|
-
...event.origin,
|
|
12413
|
+
...this.withoutHostKeys(event.serverId, event.origin),
|
|
12271
12414
|
serverId: event.serverId,
|
|
12272
12415
|
featureSet: event.featureSet,
|
|
12273
12416
|
eventId: event.eventId,
|
|
@@ -12342,6 +12485,7 @@ export class AgentFramework {
|
|
|
12342
12485
|
source: event.serverId,
|
|
12343
12486
|
timestamp: Date.now(),
|
|
12344
12487
|
channelId: triggerChannel?.channelId,
|
|
12488
|
+
serverId: triggerChannel ? event.serverId : undefined,
|
|
12345
12489
|
// DMs and addressed-while-closed messages arrive as push events;
|
|
12346
12490
|
// they must outrank ambient chatter in a batched wake's locus
|
|
12347
12491
|
// selection just like their channels/incoming counterparts.
|
|
@@ -12364,17 +12508,49 @@ export class AgentFramework {
|
|
|
12364
12508
|
* Whether a batched gate wake's addressed route may become `agentName`'s
|
|
12365
12509
|
* speech locus. The gate broadcasts to every agent, conversation forks
|
|
12366
12510
|
* included. A fork speaks only in its home channel, and a channel bound to
|
|
12367
|
-
* a fork is that fork's conversation, so no other agent takes it.
|
|
12511
|
+
* a fork is that fork's conversation, so no other agent takes it. Two
|
|
12512
|
+
* servers' channels with one id are two conversations, each bound to its
|
|
12513
|
+
* own fork with that id as its home, so a fork takes the route only where
|
|
12514
|
+
* the router binds the route's channel, its server and id, to it; a route
|
|
12515
|
+
* with no server names a fork only where one fork is bound to that id.
|
|
12368
12516
|
*/
|
|
12369
|
-
private mayTakeGateRoute(agentName: string, routeChannelId: string): boolean {
|
|
12517
|
+
private mayTakeGateRoute(agentName: string, routeChannelId: string, routeServerId?: string): boolean {
|
|
12370
12518
|
const home = this.conversationAgentHomes.get(agentName);
|
|
12371
|
-
if (home !== undefined)
|
|
12519
|
+
if (home !== undefined) {
|
|
12520
|
+
return home === routeChannelId
|
|
12521
|
+
&& this.conversationRouter?.getBinding({ serverId: routeServerId, channelId: routeChannelId })?.agentName === agentName;
|
|
12522
|
+
}
|
|
12372
12523
|
for (const boundChannel of this.conversationAgentHomes.values()) {
|
|
12373
12524
|
if (boundChannel === routeChannelId) return false;
|
|
12374
12525
|
}
|
|
12375
12526
|
return true;
|
|
12376
12527
|
}
|
|
12377
12528
|
|
|
12529
|
+
/**
|
|
12530
|
+
* The channel a push names, and a server that registers it, when another
|
|
12531
|
+
* server registers that channel id and the pushing server doesn't. A
|
|
12532
|
+
* channel's id is the agent's locus on its own, so the reply to such a
|
|
12533
|
+
* push would go out through the server that registers it: the push
|
|
12534
|
+
* boundary refuses it (PushHandler). A push naming a channel no server
|
|
12535
|
+
* registers isn't refused, and registers it for the pushing server
|
|
12536
|
+
* (ensureChannelRegistered).
|
|
12537
|
+
*/
|
|
12538
|
+
private channelOfAnotherServer(
|
|
12539
|
+
serverId: string,
|
|
12540
|
+
origin: Record<string, unknown> | undefined,
|
|
12541
|
+
): { channelId: string; holder: string } | undefined {
|
|
12542
|
+
if (!this.channelRegistry) return undefined;
|
|
12543
|
+
const channel = this.derivePushEventChannel(origin);
|
|
12544
|
+
if (!channel) return undefined;
|
|
12545
|
+
let holder: string | undefined;
|
|
12546
|
+
for (const entry of this.channelRegistry.listChannelsRaw()) {
|
|
12547
|
+
if (entry.descriptor.id !== channel.channelId) continue;
|
|
12548
|
+
if (entry.serverId === serverId) return undefined;
|
|
12549
|
+
holder ??= entry.serverId;
|
|
12550
|
+
}
|
|
12551
|
+
return holder === undefined ? undefined : { channelId: channel.channelId, holder };
|
|
12552
|
+
}
|
|
12553
|
+
|
|
12378
12554
|
/**
|
|
12379
12555
|
* Derive the MCPL composite channel id (the outbound routing locus) for a
|
|
12380
12556
|
* push event from its server-defined `origin`, if it carries one.
|
|
@@ -12699,6 +12875,7 @@ export class AgentFramework {
|
|
|
12699
12875
|
ephemeralSystemPrompt: silentOnly ? trigger?.ephemeralSystemPrompt : undefined,
|
|
12700
12876
|
silentHeartbeat: silentOnly ? trigger?.silentHeartbeat : undefined,
|
|
12701
12877
|
channelId: channelReq?.channelId,
|
|
12878
|
+
serverId: channelReq?.channelId !== undefined ? onlyServerFor(requests, channelReq.channelId) : undefined,
|
|
12702
12879
|
addressed: addressedReq !== undefined,
|
|
12703
12880
|
// A context-budget restart continues the same logical turn: it keeps
|
|
12704
12881
|
// the channel for routing but names no author — the restart is its
|
|
@@ -13145,8 +13322,8 @@ export class AgentFramework {
|
|
|
13145
13322
|
/** The drafts tool's answer to a resident's call, from the model's own
|
|
13146
13323
|
* dispatch or an agent-origin call through executeToolCall (a puppet's):
|
|
13147
13324
|
* a failure the handler throws is answered as one. */
|
|
13148
|
-
private draftsToolResult(agentName: string, rawInput: unknown): Promise<ToolResult> {
|
|
13149
|
-
return this.handleDraftsTool(agentName, rawInput).catch((err): ToolResult => ({
|
|
13325
|
+
private draftsToolResult(agentName: string, rawInput: unknown, callerTurnId?: number): Promise<ToolResult> {
|
|
13326
|
+
return this.handleDraftsTool(agentName, rawInput, callerTurnId).catch((err): ToolResult => ({
|
|
13150
13327
|
success: false,
|
|
13151
13328
|
error: `drafts failed: ${thrownText(err)}`,
|
|
13152
13329
|
isError: true,
|
|
@@ -13159,7 +13336,7 @@ export class AgentFramework {
|
|
|
13159
13336
|
* each draft is published verbatim, in order, through the normal publish
|
|
13160
13337
|
* executor, its attempt journaled durably before the request leaves.
|
|
13161
13338
|
*/
|
|
13162
|
-
private async handleDraftsTool(agentName: string, rawInput: unknown): Promise<ToolResult> {
|
|
13339
|
+
private async handleDraftsTool(agentName: string, rawInput: unknown, callerTurnId?: number): Promise<ToolResult> {
|
|
13163
13340
|
const input = (rawInput && typeof rawInput === 'object' && !Array.isArray(rawInput) ? rawInput : {}) as Record<string, unknown>;
|
|
13164
13341
|
// The turn that sends: a resent draft's channel is recorded as engaged on
|
|
13165
13342
|
// it, as a send tool's is, even when the result comes after the turn.
|
|
@@ -13326,7 +13503,17 @@ export class AgentFramework {
|
|
|
13326
13503
|
const cand = resolved.candidates?.length ? ` It could mean: ${resolved.candidates.join(', ')}.` : '';
|
|
13327
13504
|
return refuse(`Destination "${spec}" did not resolve: ${resolved.error}.${cand} Nothing was sent.`);
|
|
13328
13505
|
}
|
|
13329
|
-
|
|
13506
|
+
// A resend into a channel the calling turn recorded a server for goes
|
|
13507
|
+
// through that server (triggerServerFor), where the call was made inside
|
|
13508
|
+
// that turn; a background script's call carries no turn.
|
|
13509
|
+
const callingRouting = this.turnRouting.get(agentName);
|
|
13510
|
+
const triggerServer = callerTurnId !== undefined && callingRouting?.turnId === callerTurnId
|
|
13511
|
+
? triggerServerFor(callingRouting, resolved.channelId)
|
|
13512
|
+
: undefined;
|
|
13513
|
+
const target = registry.resolveDestination({
|
|
13514
|
+
...(triggerServer !== undefined ? { serverId: triggerServer } : {}),
|
|
13515
|
+
channelId: resolved.channelId,
|
|
13516
|
+
});
|
|
13330
13517
|
if ('error' in target) return refuse(`Destination "${spec}": ${target.error}. Nothing was sent.`);
|
|
13331
13518
|
const destination = target.destination;
|
|
13332
13519
|
if (home && destination.channelId !== home) {
|
|
@@ -13672,7 +13859,10 @@ export class AgentFramework {
|
|
|
13672
13859
|
if (!open) return;
|
|
13673
13860
|
if ((metadata as { tuneOut?: unknown } | undefined)?.tuneOut) return;
|
|
13674
13861
|
const channelId = this.repinChannel(metadata, open);
|
|
13675
|
-
if (channelId)
|
|
13862
|
+
if (channelId) {
|
|
13863
|
+
const serverId = messageServerId(metadata);
|
|
13864
|
+
open.arrivals.push({ messageId, channelId, ...(serverId !== undefined ? { serverId } : {}) });
|
|
13865
|
+
}
|
|
13676
13866
|
}
|
|
13677
13867
|
|
|
13678
13868
|
/**
|
|
@@ -13708,14 +13898,14 @@ export class AgentFramework {
|
|
|
13708
13898
|
* message in the store; the last compile can't be the test, since an
|
|
13709
13899
|
* arrival between the turn's streams isn't in it yet.
|
|
13710
13900
|
*/
|
|
13711
|
-
private remainingArrival(agent: Agent, routing: TurnRouting):
|
|
13901
|
+
private remainingArrival(agent: Agent, routing: TurnRouting): Arrival | undefined {
|
|
13712
13902
|
const cm = agent.getContextManager();
|
|
13713
13903
|
for (let i = routing.arrivals.length - 1; i >= 0; i--) {
|
|
13714
|
-
const
|
|
13904
|
+
const arrival = routing.arrivals[i]!;
|
|
13715
13905
|
// The store answers null for a message that isn't there. A store that
|
|
13716
13906
|
// can't be read throws here, as anywhere else in the start, rather
|
|
13717
13907
|
// than have its candidates read as withdrawn.
|
|
13718
|
-
if (cm.getMessage(messageId)) return
|
|
13908
|
+
if (cm.getMessage(arrival.messageId)) return arrival;
|
|
13719
13909
|
}
|
|
13720
13910
|
return undefined;
|
|
13721
13911
|
}
|
|
@@ -13737,10 +13927,12 @@ export class AgentFramework {
|
|
|
13737
13927
|
routing: TurnRouting,
|
|
13738
13928
|
channelId: string,
|
|
13739
13929
|
cause: string,
|
|
13930
|
+
/** The server of the message that moved it. */
|
|
13931
|
+
serverId?: string,
|
|
13740
13932
|
): { participant: string; content: ContentBlock[]; metadata: MessageMetadata } | undefined {
|
|
13741
13933
|
if (routing.locus === channelId) return undefined;
|
|
13742
13934
|
const prevPin = routing.locus ?? null;
|
|
13743
|
-
routing
|
|
13935
|
+
setTurnLocus(routing, channelId, serverId);
|
|
13744
13936
|
const label = this.channelRegistry?.getDescriptor(channelId)?.label;
|
|
13745
13937
|
const shown = label && label !== channelId
|
|
13746
13938
|
? `${label.startsWith('#') ? label : `#${label}`} (${channelId})`
|
|
@@ -14002,11 +14194,13 @@ export class AgentFramework {
|
|
|
14002
14194
|
body: string,
|
|
14003
14195
|
channelId: string,
|
|
14004
14196
|
unsent: Draft | undefined,
|
|
14197
|
+
/** The server the channel resolves with (triggerServerFor). */
|
|
14198
|
+
serverId: string | undefined,
|
|
14005
14199
|
): Promise<PublishOutcome> {
|
|
14006
14200
|
const registry = this.channelRegistry!;
|
|
14007
14201
|
let attemptId: string | undefined;
|
|
14008
14202
|
if (unsent) {
|
|
14009
|
-
const target = registry.resolveDestination({ channelId });
|
|
14203
|
+
const target = registry.resolveDestination({ ...(serverId !== undefined ? { serverId } : {}), channelId });
|
|
14010
14204
|
if (!('error' in target)) {
|
|
14011
14205
|
try {
|
|
14012
14206
|
attemptId = this.proseDrafts.beginAttempt(agent.name, unsent.id, target.destination, 'unsent-token', false);
|
|
@@ -14027,7 +14221,8 @@ export class AgentFramework {
|
|
|
14027
14221
|
try {
|
|
14028
14222
|
// Both callers hold a failed delivery's words as a bounce draft, whose
|
|
14029
14223
|
// notice says what happened, so the registry writes no second marker.
|
|
14030
|
-
outcome = await registry.deliverSpeech(agent.name, body, channelId,
|
|
14224
|
+
outcome = await registry.deliverSpeech(agent.name, body, channelId,
|
|
14225
|
+
{ heldOnFailure: true, ...(serverId !== undefined ? { locusServerId: serverId } : {}) });
|
|
14031
14226
|
} finally {
|
|
14032
14227
|
if (flight) {
|
|
14033
14228
|
this.draftsInFlight.delete(flight);
|
|
@@ -14125,7 +14320,7 @@ export class AgentFramework {
|
|
|
14125
14320
|
continue;
|
|
14126
14321
|
}
|
|
14127
14322
|
try {
|
|
14128
|
-
const outcome = await this.deliverWithUnsent(agent, body, resolved.channelId, unsent.draft);
|
|
14323
|
+
const outcome = await this.deliverWithUnsent(agent, body, resolved.channelId, unsent.draft, triggerServerFor(routing, resolved.channelId));
|
|
14129
14324
|
this.recordProseDelivery(routing, outcome.status === 'delivered' ? { delivered: true, channelId: outcome.destination!.channelId } : null);
|
|
14130
14325
|
if (outcome.status === 'delivered') {
|
|
14131
14326
|
this.proseBounceStreaks.delete(agent.name);
|
|
@@ -14167,7 +14362,7 @@ export class AgentFramework {
|
|
|
14167
14362
|
}
|
|
14168
14363
|
const target = sticky ?? locus;
|
|
14169
14364
|
try {
|
|
14170
|
-
const outcome = await this.channelRegistry!.routeSpeech(agent.name, envelope, target);
|
|
14365
|
+
const outcome = await this.channelRegistry!.routeSpeech(agent.name, envelope, target, speechServerOpts(routing, target));
|
|
14171
14366
|
this.recordProseDelivery(routing, outcome);
|
|
14172
14367
|
} catch (err) {
|
|
14173
14368
|
console.error('hybrid locus delivery failed:', err);
|
|
@@ -14288,7 +14483,7 @@ export class AgentFramework {
|
|
|
14288
14483
|
}
|
|
14289
14484
|
|
|
14290
14485
|
try {
|
|
14291
|
-
const outcome = await this.deliverWithUnsent(agent, body, targetChannel, unsent.draft);
|
|
14486
|
+
const outcome = await this.deliverWithUnsent(agent, body, targetChannel, unsent.draft, triggerServerFor(routing, targetChannel));
|
|
14292
14487
|
if (outcome.status === 'delivered') {
|
|
14293
14488
|
this.proseBounceStreaks.delete(name);
|
|
14294
14489
|
} else if (outcome.status === 'failed') {
|
|
@@ -14879,6 +15074,9 @@ export class AgentFramework {
|
|
|
14879
15074
|
// turn, which falls back to the global default rather than inheriting
|
|
14880
15075
|
// a previous turn's channel.
|
|
14881
15076
|
routing.triggerChannel = trigger?.channelId;
|
|
15077
|
+
routing.triggerOrigin = trigger?.channelId && trigger.serverId
|
|
15078
|
+
? { channelId: trigger.channelId, serverId: trigger.serverId }
|
|
15079
|
+
: undefined;
|
|
14882
15080
|
// FREEZE the turn's outbound locus now (turn-frozen routing): the home
|
|
14883
15081
|
// channel for forks, else the trigger channel, else the global default.
|
|
14884
15082
|
// Mid-turn injections must not redirect the agent's plain prose
|
|
@@ -14893,7 +15091,14 @@ export class AgentFramework {
|
|
|
14893
15091
|
// stability).
|
|
14894
15092
|
if (turnProseRouting !== 'explicit' && turnProseRouting !== 'disabled') {
|
|
14895
15093
|
const locus = this.channelRegistry?.resolveLocus(agent.name) ?? null;
|
|
14896
|
-
|
|
15094
|
+
// A locus that is neither the trigger's channel nor the agent's home
|
|
15095
|
+
// is the global fallback: its server is the latest accepted
|
|
15096
|
+
// message's, taken now, with the locus.
|
|
15097
|
+
if (locus !== null) {
|
|
15098
|
+
setTurnLocus(routing, locus, locus !== routing.triggerChannel && !this.conversationAgentHomes.has(agent.name)
|
|
15099
|
+
? this.channelRegistry?.fallbackServerFor(locus)
|
|
15100
|
+
: undefined);
|
|
15101
|
+
}
|
|
14897
15102
|
if (attempt === 0) this.announceLocusIfChanged(agent.name, locus);
|
|
14898
15103
|
}
|
|
14899
15104
|
} else {
|
|
@@ -14906,7 +15111,7 @@ export class AgentFramework {
|
|
|
14906
15111
|
? this.remainingArrival(agent, routing)
|
|
14907
15112
|
: undefined;
|
|
14908
15113
|
routing.arrivals = [];
|
|
14909
|
-
if (arrival) this.repinTurnLocus(agent, routing, arrival, 'arrival');
|
|
15114
|
+
if (arrival) this.repinTurnLocus(agent, routing, arrival.channelId, 'arrival', arrival.serverId);
|
|
14910
15115
|
}
|
|
14911
15116
|
// A silent start, or a disabled agent's, has no locus to show, whatever a
|
|
14912
15117
|
// channel_open in a silent turn left on its object.
|
|
@@ -15386,7 +15591,7 @@ export class AgentFramework {
|
|
|
15386
15591
|
};
|
|
15387
15592
|
const enqueueSpeech = (text: string, locus: string | null): void => {
|
|
15388
15593
|
chainSpeech(async () => {
|
|
15389
|
-
const outcome = await this.channelRegistry!.routeSpeech(agent.name, text, locus);
|
|
15594
|
+
const outcome = await this.channelRegistry!.routeSpeech(agent.name, text, locus, speechServerOpts(streamRouting, locus));
|
|
15390
15595
|
this.recordProseDelivery(streamRouting, outcome);
|
|
15391
15596
|
}, 'mid-turn speech routing failed:');
|
|
15392
15597
|
};
|
|
@@ -15506,6 +15711,7 @@ export class AgentFramework {
|
|
|
15506
15711
|
for (const rd of deltas) {
|
|
15507
15712
|
this.channelRegistry!.sendOutgoingChunk(
|
|
15508
15713
|
rd.channelId, agent.name, outgoingInferenceId, outgoingIndex++, rd.delta,
|
|
15714
|
+
triggerServerFor(streamRouting, rd.channelId),
|
|
15509
15715
|
);
|
|
15510
15716
|
}
|
|
15511
15717
|
};
|
|
@@ -16311,7 +16517,7 @@ export class AgentFramework {
|
|
|
16311
16517
|
);
|
|
16312
16518
|
for (const segment of speechSegments) {
|
|
16313
16519
|
try {
|
|
16314
|
-
const outcome = await this.channelRegistry.routeSpeech(agent.name, segment, locus);
|
|
16520
|
+
const outcome = await this.channelRegistry.routeSpeech(agent.name, segment, locus, speechServerOpts(streamRouting, locus));
|
|
16315
16521
|
this.recordProseDelivery(streamRouting, outcome);
|
|
16316
16522
|
} catch (err) {
|
|
16317
16523
|
console.error('speech routing failed:', err);
|
|
@@ -16407,7 +16613,7 @@ export class AgentFramework {
|
|
|
16407
16613
|
// Deliver sequentially (await each) so the segments land in order.
|
|
16408
16614
|
for (const seg of segments) {
|
|
16409
16615
|
try {
|
|
16410
|
-
const outcome = await this.channelRegistry.routeSpeech(agent.name, seg, locus);
|
|
16616
|
+
const outcome = await this.channelRegistry.routeSpeech(agent.name, seg, locus, speechServerOpts(streamRouting, locus));
|
|
16411
16617
|
this.recordProseDelivery(streamRouting, outcome);
|
|
16412
16618
|
} catch (err) {
|
|
16413
16619
|
console.error('speech routing failed:', err);
|
|
@@ -17051,7 +17257,7 @@ export class AgentFramework {
|
|
|
17051
17257
|
if (frameReachedTerminal && proseStream) {
|
|
17052
17258
|
emitOutgoing(proseStream.finish());
|
|
17053
17259
|
for (const [channelId, text] of proseStream.byChannel()) {
|
|
17054
|
-
this.channelRegistry!.sendOutgoingComplete(channelId, agent.name, outgoingInferenceId, text);
|
|
17260
|
+
this.channelRegistry!.sendOutgoingComplete(channelId, agent.name, outgoingInferenceId, text, triggerServerFor(streamRouting, channelId));
|
|
17055
17261
|
}
|
|
17056
17262
|
}
|
|
17057
17263
|
this.frameworkCancelledStreams.delete(`${agent.name}:${myStreamId}`);
|
|
@@ -17343,7 +17549,7 @@ export class AgentFramework {
|
|
|
17343
17549
|
// agent origin and acts as that resident, as the model's own call does
|
|
17344
17550
|
// through dispatchToolCall. A module holds no drafts.
|
|
17345
17551
|
if (call.name === 'drafts' && this.channelRegistry && origin.kind === 'agent') {
|
|
17346
|
-
return this.draftsToolResult(origin.agentName, call.input);
|
|
17552
|
+
return this.draftsToolResult(origin.agentName, call.input, call.callerTurnId);
|
|
17347
17553
|
}
|
|
17348
17554
|
|
|
17349
17555
|
// The utils meta-tool — programmatic callers (code_execution scripts,
|
|
@@ -18813,7 +19019,7 @@ export class AgentFramework {
|
|
|
18813
19019
|
if (enrichedCall.name === 'drafts' && this.channelRegistry) {
|
|
18814
19020
|
const startedAt = Date.now();
|
|
18815
19021
|
this.emitTrace({ type: 'tool:started', module: 'framework', tool: 'drafts', callId: enrichedCall.id, input: enrichedCall.input });
|
|
18816
|
-
void this.draftsToolResult(agentName, enrichedCall.input)
|
|
19022
|
+
void this.draftsToolResult(agentName, enrichedCall.input, enrichedCall.callerTurnId)
|
|
18817
19023
|
.then((result) => {
|
|
18818
19024
|
this.emitTrace({ type: 'tool:completed', module: 'framework', tool: 'drafts', callId: enrichedCall.id, durationMs: Date.now() - startedAt });
|
|
18819
19025
|
this.pushEvent({ type: 'tool-result', callId: enrichedCall.id, toolName: enrichedCall.name, agentName, moduleName: 'framework', result });
|
|
@@ -19965,6 +20171,7 @@ export class AgentFramework {
|
|
|
19965
20171
|
this.noteInboundAccepted(source);
|
|
19966
20172
|
return source;
|
|
19967
20173
|
},
|
|
20174
|
+
(serverId, params) => this.channelOfAnotherServer(serverId, params.origin),
|
|
19968
20175
|
);
|
|
19969
20176
|
|
|
19970
20177
|
// Server-initiated inference router (Step 6)
|
|
@@ -22423,8 +22630,17 @@ export class AgentFramework {
|
|
|
22423
22630
|
// ordinary wake, starts only once its over-budget boundary has every
|
|
22424
22631
|
// result of the round.
|
|
22425
22632
|
const callingTurn = this.routingOf(agentName);
|
|
22633
|
+
const inTurn = call.callerTurnId !== undefined && call.callerTurnId === callingTurn.turnId;
|
|
22426
22634
|
|
|
22427
|
-
this.channelRegistry!.handleChannelToolCall(call.name, call.input, {
|
|
22635
|
+
this.channelRegistry!.handleChannelToolCall(call.name, call.input, {
|
|
22636
|
+
kind: 'agent',
|
|
22637
|
+
agentName,
|
|
22638
|
+
// The turn's recorded servers go only with a call made inside that
|
|
22639
|
+
// turn: a background script's call carries no turn, and takes a bare
|
|
22640
|
+
// id only where exactly one server registers it.
|
|
22641
|
+
...(inTurn && callingTurn.triggerOrigin !== undefined ? { trigger: callingTurn.triggerOrigin } : {}),
|
|
22642
|
+
...(inTurn && callingTurn.locusOrigin !== undefined ? { locus: callingTurn.locusOrigin } : {}),
|
|
22643
|
+
})
|
|
22428
22644
|
.then((result) => {
|
|
22429
22645
|
const durationMs = Date.now() - startTime;
|
|
22430
22646
|
this.emitTrace({
|
|
@@ -22462,7 +22678,13 @@ export class AgentFramework {
|
|
|
22462
22678
|
if (openerAgent
|
|
22463
22679
|
&& (openerAgent.proseRouting === 'locus' || openerAgent.proseRouting === 'hybrid')
|
|
22464
22680
|
&& this.openLogical.get(agentName) === callingTurn) {
|
|
22465
|
-
|
|
22681
|
+
const openedOn = this.channelRegistry!.resolveDestination({
|
|
22682
|
+
...(typeof (call.input as { serverId?: unknown } | undefined)?.serverId === 'string'
|
|
22683
|
+
? { serverId: (call.input as { serverId: string }).serverId }
|
|
22684
|
+
: {}),
|
|
22685
|
+
channelId: opened,
|
|
22686
|
+
});
|
|
22687
|
+
setTurnLocus(callingTurn, opened, 'error' in openedOn ? undefined : openedOn.destination.serverId);
|
|
22466
22688
|
this.lastAnnouncedLocus.set(agentName, opened);
|
|
22467
22689
|
result = {
|
|
22468
22690
|
...result,
|
|
@@ -22736,17 +22958,21 @@ export class AgentFramework {
|
|
|
22736
22958
|
} else if (agent?.proseRouting === 'explicit') {
|
|
22737
22959
|
// Explicit mode: announce to the turn's sticky prose target if the
|
|
22738
22960
|
// model has set one; otherwise stay quiet — never guess a channel.
|
|
22739
|
-
const
|
|
22961
|
+
const routing = this.turnRouting.get(agentName);
|
|
22962
|
+
const target = routing?.pin;
|
|
22740
22963
|
if (target) {
|
|
22741
|
-
this.channelRegistry.routeSpeech(agentName, text, target
|
|
22964
|
+
this.channelRegistry.routeSpeech(agentName, text, target,
|
|
22965
|
+
speechServerOpts(call.callerTurnId === routing?.turnId ? routing : undefined, target)).catch((err) => {
|
|
22742
22966
|
console.error('[sleep] announce failed:', err instanceof Error ? err.message : err);
|
|
22743
22967
|
});
|
|
22744
22968
|
} else {
|
|
22745
22969
|
console.error(`[sleep] ${agentName}: no prose target this turn — sleep announcement not posted (explicit mode never guesses)`);
|
|
22746
22970
|
}
|
|
22747
22971
|
} else {
|
|
22748
|
-
const
|
|
22749
|
-
|
|
22972
|
+
const routing = this.turnRouting.get(agentName);
|
|
22973
|
+
const locus = routing?.locus ?? null;
|
|
22974
|
+
this.channelRegistry.routeSpeech(agentName, text, locus,
|
|
22975
|
+
speechServerOpts(call.callerTurnId === routing?.turnId ? routing : undefined, locus)).catch((err) => {
|
|
22750
22976
|
console.error('[sleep] announce failed:', err instanceof Error ? err.message : err);
|
|
22751
22977
|
});
|
|
22752
22978
|
}
|