@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/src/framework.ts
CHANGED
|
@@ -282,6 +282,28 @@ const silenceCauseOf = (calls: Array<{ name: string; input?: unknown }>): 'send'
|
|
|
282
282
|
return calls.some(isSilencingCall) ? 'send' : null;
|
|
283
283
|
};
|
|
284
284
|
|
|
285
|
+
/**
|
|
286
|
+
* The keys of a stored message's metadata that only the host writes and that
|
|
287
|
+
* some reader takes on its own as what the message is: `system` and `kind`
|
|
288
|
+
* make it a host notice of that kind, `draftOwner` and `draftIds` one that
|
|
289
|
+
* names those drafts, `tuneOut` a message a tune-out holds from the
|
|
290
|
+
* residents, `coalescingSubject` (beside the occurrence's `eventId`) the
|
|
291
|
+
* delivery of a coalesced occurrence, `deferredWriteId` the landed copy of a
|
|
292
|
+
* deferred write, `external` a module's own row for an outside message, and
|
|
293
|
+
* `openaiResponsesItems` membrane's mark of an imported Responses history. A
|
|
294
|
+
* server's metadata, a channel message's `metadata` or a push event's
|
|
295
|
+
* `origin`, is stored without them, and the host writes its own after it:
|
|
296
|
+
* MCPL SPEC §16.6 makes metadata the producer's untrusted claims, which a
|
|
297
|
+
* host may disbelieve. Keys a server writes by protocol, such as `tags` and
|
|
298
|
+
* `mentioned`, stay. So do the keys a reader takes only beside one of these:
|
|
299
|
+
* a failed reply's `reason` and `detail` beside its `kind`, and the Discord
|
|
300
|
+
* module's `discordTextFirst` on a row it found by `external`.
|
|
301
|
+
*/
|
|
302
|
+
const HOST_METADATA_KEYS = [
|
|
303
|
+
'system', 'kind', 'draftOwner', 'draftIds', 'tuneOut', 'coalescingSubject', 'deferredWriteId',
|
|
304
|
+
'external', 'openaiResponsesItems',
|
|
305
|
+
] as const;
|
|
306
|
+
|
|
285
307
|
/**
|
|
286
308
|
* True when an injected message is real conversational input — something the
|
|
287
309
|
* agent might actually be replying to — rather than ambient machinery.
|
|
@@ -779,6 +801,31 @@ interface TurnRouting {
|
|
|
779
801
|
* sent Sol's DM reply to a stale guild channel (2026-07-22).
|
|
780
802
|
*/
|
|
781
803
|
triggerChannel?: string;
|
|
804
|
+
/**
|
|
805
|
+
* The trigger's channel with the server whose message it was, frozen when
|
|
806
|
+
* the turn opens and never moved: a channel_open moves triggerChannel and
|
|
807
|
+
* a re-pin moves the locus, but neither carries this server with it. Every
|
|
808
|
+
* send of the turn whose destination id is this channel resolves with this
|
|
809
|
+
* server (triggerServerFor): speech, a channel_publish naming it or taking
|
|
810
|
+
* it as its default, a drafts resend, and the refusal reaction. So a
|
|
811
|
+
* message one server sent can't have its reply go out through another
|
|
812
|
+
* server that registers the same id, whatever the registry holds by then;
|
|
813
|
+
* the reply is refused where its server no longer registers the channel.
|
|
814
|
+
* Unset for a turn with no channel trigger, and where the requests batched
|
|
815
|
+
* into the trigger name its channel from more than one server: a bare id
|
|
816
|
+
* then resolves only where exactly one server registers it.
|
|
817
|
+
*/
|
|
818
|
+
triggerOrigin?: { channelId: string; serverId: string };
|
|
819
|
+
/**
|
|
820
|
+
* The locus with the server of the message that put it there, where that
|
|
821
|
+
* isn't the trigger: the latest accepted message's (the global fallback a
|
|
822
|
+
* turn with no trigger channel takes), an arrival's or a mid-turn
|
|
823
|
+
* message's (a re-pin), or the server a channel_open opened it on. Set
|
|
824
|
+
* with the locus (setTurnLocus), and cleared when the locus moves to a
|
|
825
|
+
* channel no one server put there. A send into this channel resolves with
|
|
826
|
+
* this server, as one into the trigger's channel does with the trigger's.
|
|
827
|
+
*/
|
|
828
|
+
locusOrigin?: { channelId: string; serverId: string };
|
|
782
829
|
/**
|
|
783
830
|
* Locus and hybrid mode: where the turn's unpinned plain prose lands.
|
|
784
831
|
* Frozen when the turn opens (home channel for a fork, else the trigger
|
|
@@ -855,11 +902,60 @@ interface Arrival {
|
|
|
855
902
|
messageId: MessageId;
|
|
856
903
|
/** The channel it re-pins the locus to. */
|
|
857
904
|
channelId: string;
|
|
905
|
+
/** The server whose message it is. */
|
|
906
|
+
serverId?: string;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/** The server a stored channel message came from, as the host wrote it. */
|
|
910
|
+
function messageServerId(metadata: MessageMetadata | undefined): string | undefined {
|
|
911
|
+
const serverId = (metadata as { serverId?: unknown } | undefined)?.serverId;
|
|
912
|
+
return typeof serverId === 'string' && serverId ? serverId : undefined;
|
|
858
913
|
}
|
|
859
914
|
|
|
860
915
|
/** The last TurnRouting.turnId given out: ids increase, and are unique within the process. */
|
|
861
916
|
let lastTurnId = 0;
|
|
862
917
|
|
|
918
|
+
/**
|
|
919
|
+
* The server every request naming `channelId` came from, or undefined where
|
|
920
|
+
* one names no server or they name more than one (TurnRouting.triggerOrigin).
|
|
921
|
+
*/
|
|
922
|
+
function onlyServerFor(requests: readonly InferenceRequest[], channelId: string): string | undefined {
|
|
923
|
+
let serverId: string | undefined;
|
|
924
|
+
for (const r of requests) {
|
|
925
|
+
if (r.channelId !== channelId) continue;
|
|
926
|
+
if (!r.serverId || (serverId !== undefined && r.serverId !== serverId)) return undefined;
|
|
927
|
+
serverId = r.serverId;
|
|
928
|
+
}
|
|
929
|
+
return serverId;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* The server a send in `routing`'s turn resolves `channelId` with: the
|
|
934
|
+
* trigger's, where `channelId` is the trigger's channel
|
|
935
|
+
* (TurnRouting.triggerOrigin), else the locus's, where it's the channel the
|
|
936
|
+
* locus's server put there (TurnRouting.locusOrigin).
|
|
937
|
+
*/
|
|
938
|
+
function triggerServerFor(routing: TurnRouting | undefined, channelId: string | null | undefined): string | undefined {
|
|
939
|
+
if (routing === undefined || !channelId) return undefined;
|
|
940
|
+
if (routing.triggerOrigin?.channelId === channelId) return routing.triggerOrigin.serverId;
|
|
941
|
+
if (routing.locusOrigin?.channelId === channelId) return routing.locusOrigin.serverId;
|
|
942
|
+
return undefined;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/** Move a turn's locus, with the server of the message that put it there
|
|
946
|
+
* (TurnRouting.locusOrigin), or none. */
|
|
947
|
+
function setTurnLocus(routing: TurnRouting, channelId: string, serverId: string | undefined): void {
|
|
948
|
+
routing.locus = channelId;
|
|
949
|
+
routing.locusOrigin = serverId !== undefined ? { channelId, serverId } : undefined;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/** routeSpeech's options for speech into `channelId` in `routing`'s turn:
|
|
953
|
+
* the trigger's server, where it's the trigger's channel (triggerServerFor). */
|
|
954
|
+
function speechServerOpts(routing: TurnRouting | undefined, channelId: string | null | undefined): { locusServerId: string } | undefined {
|
|
955
|
+
const serverId = triggerServerFor(routing, channelId);
|
|
956
|
+
return serverId !== undefined ? { locusServerId: serverId } : undefined;
|
|
957
|
+
}
|
|
958
|
+
|
|
863
959
|
function freshTurnRouting(): TurnRouting {
|
|
864
960
|
return {
|
|
865
961
|
turnId: ++lastTurnId,
|
|
@@ -2929,8 +3025,8 @@ export class AgentFramework {
|
|
|
2929
3025
|
// route applies to a fork only when it IS that home, and never to
|
|
2930
3026
|
// another agent when a fork owns that channel. Otherwise a fork
|
|
2931
3027
|
// bound elsewhere would show typing where it isn't replying.
|
|
2932
|
-
...(provenance?.routeChannelId && framework.mayTakeGateRoute(agentName, provenance.routeChannelId)
|
|
2933
|
-
? { channelId: provenance.routeChannelId, addressed: true }
|
|
3028
|
+
...(provenance?.routeChannelId && framework.mayTakeGateRoute(agentName, provenance.routeChannelId, provenance.routeServerId)
|
|
3029
|
+
? { channelId: provenance.routeChannelId, serverId: provenance.routeServerId, addressed: true }
|
|
2934
3030
|
: {}),
|
|
2935
3031
|
});
|
|
2936
3032
|
},
|
|
@@ -8486,7 +8582,9 @@ export class AgentFramework {
|
|
|
8486
8582
|
// tools/call directly on its connection (bare tool name — no prefix
|
|
8487
8583
|
// games), bypassing the agent event queue so no synthetic tool-result
|
|
8488
8584
|
// is injected into a turn the agent never took.
|
|
8489
|
-
|
|
8585
|
+
// Through the server whose message it marks: the latest accepted
|
|
8586
|
+
// message's, kept beside its channel and id (fallbackServerFor).
|
|
8587
|
+
const serverId = this.channelRegistry?.fallbackServerFor(incoming.channelId);
|
|
8490
8588
|
const server = serverId ? this.mcplServerRegistry?.getServer(serverId) : null;
|
|
8491
8589
|
if (!server) {
|
|
8492
8590
|
console.error(
|
|
@@ -10852,9 +10950,9 @@ export class AgentFramework {
|
|
|
10852
10950
|
// turn ends. The window stores them here, right after the
|
|
10853
10951
|
// tool_result message, which is exactly where the membrane appends
|
|
10854
10952
|
// them in the live conversation — same participant, same content
|
|
10855
|
-
// blocks,
|
|
10856
|
-
// verbatim, so the next turn's compiled prefix
|
|
10857
|
-
// live request (prompt-cache safe).
|
|
10953
|
+
// blocks, and neither carries the row's metadata — and compile()
|
|
10954
|
+
// emits recent messages verbatim, so the next turn's compiled prefix
|
|
10955
|
+
// byte-matches the live request (prompt-cache safe).
|
|
10858
10956
|
//
|
|
10859
10957
|
// The queue is drained ONLY at the target agent's own boundary:
|
|
10860
10958
|
// draining at another agent's boundary would store the messages
|
|
@@ -11028,12 +11126,19 @@ export class AgentFramework {
|
|
|
11028
11126
|
// The last injection that qualifies wins (repinChannel).
|
|
11029
11127
|
const routing = this.routingOf(agent.name);
|
|
11030
11128
|
let newLocus: string | undefined;
|
|
11031
|
-
|
|
11129
|
+
let newLocusServer: string | undefined;
|
|
11130
|
+
for (const inj of midTurnInjections) {
|
|
11131
|
+
const channelId = this.repinChannel(inj.metadata, routing);
|
|
11132
|
+
if (channelId) {
|
|
11133
|
+
newLocus = channelId;
|
|
11134
|
+
newLocusServer = messageServerId(inj.metadata);
|
|
11135
|
+
}
|
|
11136
|
+
}
|
|
11032
11137
|
if (newLocus) {
|
|
11033
11138
|
// What this boundary just stored is newer than any arrival the
|
|
11034
11139
|
// turn holds, so the arrivals can't move the locus back.
|
|
11035
11140
|
routing.arrivals = [];
|
|
11036
|
-
const notice = this.repinTurnLocus(agent, routing, newLocus, 'mid-turn addressed');
|
|
11141
|
+
const notice = this.repinTurnLocus(agent, routing, newLocus, 'mid-turn addressed', newLocusServer);
|
|
11037
11142
|
if (notice) midTurnInjections.push(notice);
|
|
11038
11143
|
}
|
|
11039
11144
|
} else if (routesLocus && !shouldEndTurn) {
|
|
@@ -11109,10 +11214,14 @@ export class AgentFramework {
|
|
|
11109
11214
|
// Streaming path: convert results and resume the stream.
|
|
11110
11215
|
// Mid-turn messages collected above ride along as injected user
|
|
11111
11216
|
// messages (membrane ≥0.5.72) — appended after the tool_result
|
|
11112
|
-
// envelope so the next round of THIS turn hears them.
|
|
11217
|
+
// envelope so the next round of THIS turn hears them. Each goes
|
|
11218
|
+
// as its participant and content alone: a stored row's metadata
|
|
11219
|
+
// is the host's record, not input for membrane.
|
|
11113
11220
|
currentState.stream.provideToolResults(
|
|
11114
11221
|
agent.toolResultGuard.submissionResults(membraneResults),
|
|
11115
|
-
midTurnInjections.length > 0
|
|
11222
|
+
midTurnInjections.length > 0
|
|
11223
|
+
? { injectedMessages: midTurnInjections.map(({ participant, content }) => ({ participant, content })) }
|
|
11224
|
+
: undefined,
|
|
11116
11225
|
);
|
|
11117
11226
|
// RFC-006 §3.3: consumption is determined at assembly, and this
|
|
11118
11227
|
// is assembly too when the stream carries the injected messages
|
|
@@ -11313,6 +11422,31 @@ export class AgentFramework {
|
|
|
11313
11422
|
// Built-in MCPL event → context message conversion
|
|
11314
11423
|
// ==========================================================================
|
|
11315
11424
|
|
|
11425
|
+
/** The servers and keys whose drop withoutHostKeys has logged, each as
|
|
11426
|
+
* `${serverId}\u0000${key}`. */
|
|
11427
|
+
private readonly hostKeysDropped = new Set<string>();
|
|
11428
|
+
|
|
11429
|
+
/**
|
|
11430
|
+
* A server's metadata as it is stored: a copy without HOST_METADATA_KEYS.
|
|
11431
|
+
* A server may use one of those names for something of its own, so the
|
|
11432
|
+
* first drop of each key from each server is logged.
|
|
11433
|
+
*/
|
|
11434
|
+
private withoutHostKeys(serverId: string, given: Record<string, unknown> | undefined): Record<string, unknown> {
|
|
11435
|
+
const kept: Record<string, unknown> = { ...given };
|
|
11436
|
+
for (const key of HOST_METADATA_KEYS) {
|
|
11437
|
+
if (!Object.hasOwn(kept, key)) continue;
|
|
11438
|
+
delete kept[key];
|
|
11439
|
+
const logged = `${serverId}\u0000${key}`;
|
|
11440
|
+
if (this.hostKeysDropped.has(logged)) continue;
|
|
11441
|
+
this.hostKeysDropped.add(logged);
|
|
11442
|
+
console.error(
|
|
11443
|
+
`[mcpl] ${serverId}: dropped "${key}" from a message, since only the host writes that key ` +
|
|
11444
|
+
`(logged once for each server and key)`,
|
|
11445
|
+
);
|
|
11446
|
+
}
|
|
11447
|
+
return kept;
|
|
11448
|
+
}
|
|
11449
|
+
|
|
11316
11450
|
/**
|
|
11317
11451
|
* Convert an incoming MCPL channel message to a context message.
|
|
11318
11452
|
* This replaces the old MCPLModule.onProcess() message conversion.
|
|
@@ -11349,7 +11483,7 @@ export class AgentFramework {
|
|
|
11349
11483
|
return undefined;
|
|
11350
11484
|
}
|
|
11351
11485
|
const metadata: Record<string, unknown> = {
|
|
11352
|
-
...event.metadata,
|
|
11486
|
+
...this.withoutHostKeys(event.serverId, event.metadata),
|
|
11353
11487
|
...(event.eventId ? { eventId: event.eventId } : {}),
|
|
11354
11488
|
...(event.coalescingSubject ? { coalescingSubject: event.coalescingSubject } : {}),
|
|
11355
11489
|
channelId: event.channelId,
|
|
@@ -11389,14 +11523,15 @@ export class AgentFramework {
|
|
|
11389
11523
|
const id = target.getContextManager().addMessage('user', event.content, metadata);
|
|
11390
11524
|
// A correction may target an older engagement. Refresh only the
|
|
11391
11525
|
// binding whose agent actually received the message.
|
|
11392
|
-
|
|
11393
|
-
|
|
11526
|
+
const channel = { serverId: event.serverId, channelId: event.channelId };
|
|
11527
|
+
if (this.conversationRouter.getBinding(channel)?.agentName === target.name) {
|
|
11528
|
+
this.conversationRouter.touch(channel);
|
|
11394
11529
|
}
|
|
11395
11530
|
this.emitTrace({ type: 'message:added', messageId: id, source: 'mcpl:channel-incoming' });
|
|
11396
11531
|
if (event.triggerInference) {
|
|
11397
11532
|
this.pendingRequests.push({
|
|
11398
11533
|
agentName: target.name, reason: 'mcpl:channel-incoming', source: event.serverId, timestamp: Date.now(),
|
|
11399
|
-
channelId: event.channelId,
|
|
11534
|
+
channelId: event.channelId, serverId: event.serverId,
|
|
11400
11535
|
counterparty: event.author?.id ? `${event.serverId}:user:${event.author.id}` : undefined,
|
|
11401
11536
|
addressed: isAddressedMessage(event.tags, event.metadata),
|
|
11402
11537
|
...(event.coalescingSubject ? { coalescingSubject: event.coalescingSubject } : {}),
|
|
@@ -11475,8 +11610,10 @@ export class AgentFramework {
|
|
|
11475
11610
|
source: event.serverId,
|
|
11476
11611
|
timestamp: Date.now(),
|
|
11477
11612
|
// Route this turn's auto-published speech back to THIS channel, not
|
|
11478
|
-
// the global most-recent-inbound locus (item-3 redux, trunk agents)
|
|
11613
|
+
// the global most-recent-inbound locus (item-3 redux, trunk agents),
|
|
11614
|
+
// through the server that sent it.
|
|
11479
11615
|
channelId: event.channelId,
|
|
11616
|
+
serverId: event.serverId,
|
|
11480
11617
|
counterparty: event.author?.id ? `${event.serverId}:user:${event.author.id}` : undefined,
|
|
11481
11618
|
// Addressed messages outrank ambient chatter when a batched wake
|
|
11482
11619
|
// picks the turn's frozen speech locus.
|
|
@@ -11554,7 +11691,7 @@ export class AgentFramework {
|
|
|
11554
11691
|
if (!agent) {
|
|
11555
11692
|
// Bound agent vanished (e.g. external reset) — unbind so the next
|
|
11556
11693
|
// qualifying message respawns a fresh fork.
|
|
11557
|
-
router.unbind(event.channelId);
|
|
11694
|
+
router.unbind({ serverId: event.serverId, channelId: event.channelId });
|
|
11558
11695
|
this.emitTrace({
|
|
11559
11696
|
type: 'mcpl:conversation-binding-orphaned',
|
|
11560
11697
|
channelId: event.channelId,
|
|
@@ -11570,7 +11707,7 @@ export class AgentFramework {
|
|
|
11570
11707
|
messageMetadata.triggered = trigger;
|
|
11571
11708
|
|
|
11572
11709
|
const id = agent.getContextManager().addMessage('user', event.content, messageMetadata);
|
|
11573
|
-
router.touch(event.channelId);
|
|
11710
|
+
router.touch({ serverId: event.serverId, channelId: event.channelId });
|
|
11574
11711
|
this.emitTrace({ type: 'message:added', messageId: id, source: 'mcpl:channel-incoming' });
|
|
11575
11712
|
|
|
11576
11713
|
if (trigger) {
|
|
@@ -11582,6 +11719,7 @@ export class AgentFramework {
|
|
|
11582
11719
|
// A fork's home channel wins in routeSpeech regardless, but carry the
|
|
11583
11720
|
// triggering channel too so the trunk/active path stays consistent.
|
|
11584
11721
|
channelId: event.channelId,
|
|
11722
|
+
serverId: event.serverId,
|
|
11585
11723
|
counterparty: event.author?.id ? `${event.serverId}:user:${event.author.id}` : undefined,
|
|
11586
11724
|
addressed: isAddressedMessage(event.tags, event.metadata),
|
|
11587
11725
|
...(event.coalescingSubject ? { coalescingSubject: event.coalescingSubject } : {}),
|
|
@@ -11726,7 +11864,7 @@ export class AgentFramework {
|
|
|
11726
11864
|
}
|
|
11727
11865
|
const decision = this.conversationRouter!.route(facts);
|
|
11728
11866
|
if (decision.kind !== 'spawn') return decision;
|
|
11729
|
-
return { ...decision, spawned: this.spawnConversationAgent(facts
|
|
11867
|
+
return { ...decision, spawned: this.spawnConversationAgent(facts, decision) };
|
|
11730
11868
|
}
|
|
11731
11869
|
}
|
|
11732
11870
|
|
|
@@ -11742,11 +11880,12 @@ export class AgentFramework {
|
|
|
11742
11880
|
* so the entry it removes is its own. It's removed before anyone waiting
|
|
11743
11881
|
* on it resumes, so a waiter that decides again doesn't find it.
|
|
11744
11882
|
*/
|
|
11745
|
-
private spawnConversationAgent(
|
|
11883
|
+
private spawnConversationAgent(facts: IncomingMessageFacts, decision: { agentName: string; generation: number }): Promise<Agent> {
|
|
11884
|
+
const channelId = facts.channelId;
|
|
11746
11885
|
const spawn = (async () => {
|
|
11747
11886
|
this.storeConversationGeneration(channelId, decision.generation);
|
|
11748
11887
|
const agent = await this.createConversationAgent(decision.agentName, channelId);
|
|
11749
|
-
this.conversationRouter!.bind(channelId, decision.agentName, decision.generation);
|
|
11888
|
+
this.conversationRouter!.bind({ serverId: facts.serverId, channelId }, decision.agentName, decision.generation);
|
|
11750
11889
|
return agent;
|
|
11751
11890
|
})();
|
|
11752
11891
|
const done = (): void => { this.conversationSpawns.delete(channelId); };
|
|
@@ -11876,7 +12015,7 @@ export class AgentFramework {
|
|
|
11876
12015
|
this.lastConversationSweep = now;
|
|
11877
12016
|
|
|
11878
12017
|
for (const binding of this.conversationRouter.expired(now)) {
|
|
11879
|
-
this.conversationRouter.unbind(binding
|
|
12018
|
+
this.conversationRouter.unbind(binding);
|
|
11880
12019
|
this.emitTrace({
|
|
11881
12020
|
type: 'mcpl:conversation-closed',
|
|
11882
12021
|
channelId: binding.channelId,
|
|
@@ -12556,7 +12695,7 @@ export class AgentFramework {
|
|
|
12556
12695
|
if (explicit) return explicit.filter((n) => this.agents.has(n));
|
|
12557
12696
|
if (occ.scope.kind === 'channel' && this.conversationRouter) {
|
|
12558
12697
|
const router = this.conversationRouter;
|
|
12559
|
-
const bound = router.getBinding(occ.scope.id)?.agentName;
|
|
12698
|
+
const bound = router.getBinding({ serverId: occ.serverId, channelId: occ.scope.id })?.agentName;
|
|
12560
12699
|
if (bound) return this.agents.has(bound) ? [bound] : [];
|
|
12561
12700
|
// No fork yet: the same bind decision an incoming message would get. A
|
|
12562
12701
|
// batch that qualifies spawns its fork now so it has someone to wake.
|
|
@@ -12672,6 +12811,7 @@ export class AgentFramework {
|
|
|
12672
12811
|
source: occ.serverId,
|
|
12673
12812
|
timestamp: Date.now(),
|
|
12674
12813
|
channelId,
|
|
12814
|
+
serverId: channelId !== undefined ? occ.serverId : undefined,
|
|
12675
12815
|
counterparty: authorId ? `${occ.serverId}:user:${authorId}` : undefined,
|
|
12676
12816
|
addressed: isAddressedMessage(occ.tags, origin),
|
|
12677
12817
|
...(plain ? {} : { coalescingSubject: subject, coalescingBatch: true }),
|
|
@@ -12810,7 +12950,7 @@ export class AgentFramework {
|
|
|
12810
12950
|
}
|
|
12811
12951
|
|
|
12812
12952
|
const metadata: Record<string, unknown> = {
|
|
12813
|
-
...event.origin,
|
|
12953
|
+
...this.withoutHostKeys(event.serverId, event.origin),
|
|
12814
12954
|
serverId: event.serverId,
|
|
12815
12955
|
featureSet: event.featureSet,
|
|
12816
12956
|
eventId: event.eventId,
|
|
@@ -12885,6 +13025,7 @@ export class AgentFramework {
|
|
|
12885
13025
|
source: event.serverId,
|
|
12886
13026
|
timestamp: Date.now(),
|
|
12887
13027
|
channelId: triggerChannel?.channelId,
|
|
13028
|
+
serverId: triggerChannel ? event.serverId : undefined,
|
|
12888
13029
|
// DMs and addressed-while-closed messages arrive as push events;
|
|
12889
13030
|
// they must outrank ambient chatter in a batched wake's locus
|
|
12890
13031
|
// selection just like their channels/incoming counterparts.
|
|
@@ -12909,17 +13050,49 @@ export class AgentFramework {
|
|
|
12909
13050
|
* Whether a batched gate wake's addressed route may become `agentName`'s
|
|
12910
13051
|
* speech locus. The gate broadcasts to every agent, conversation forks
|
|
12911
13052
|
* included. A fork speaks only in its home channel, and a channel bound to
|
|
12912
|
-
* a fork is that fork's conversation, so no other agent takes it.
|
|
13053
|
+
* a fork is that fork's conversation, so no other agent takes it. Two
|
|
13054
|
+
* servers' channels with one id are two conversations, each bound to its
|
|
13055
|
+
* own fork with that id as its home, so a fork takes the route only where
|
|
13056
|
+
* the router binds the route's channel, its server and id, to it; a route
|
|
13057
|
+
* with no server names a fork only where one fork is bound to that id.
|
|
12913
13058
|
*/
|
|
12914
|
-
private mayTakeGateRoute(agentName: string, routeChannelId: string): boolean {
|
|
13059
|
+
private mayTakeGateRoute(agentName: string, routeChannelId: string, routeServerId?: string): boolean {
|
|
12915
13060
|
const home = this.conversationAgentHomes.get(agentName);
|
|
12916
|
-
if (home !== undefined)
|
|
13061
|
+
if (home !== undefined) {
|
|
13062
|
+
return home === routeChannelId
|
|
13063
|
+
&& this.conversationRouter?.getBinding({ serverId: routeServerId, channelId: routeChannelId })?.agentName === agentName;
|
|
13064
|
+
}
|
|
12917
13065
|
for (const boundChannel of this.conversationAgentHomes.values()) {
|
|
12918
13066
|
if (boundChannel === routeChannelId) return false;
|
|
12919
13067
|
}
|
|
12920
13068
|
return true;
|
|
12921
13069
|
}
|
|
12922
13070
|
|
|
13071
|
+
/**
|
|
13072
|
+
* The channel a push names, and a server that registers it, when another
|
|
13073
|
+
* server registers that channel id and the pushing server doesn't. A
|
|
13074
|
+
* channel's id is the agent's locus on its own, so the reply to such a
|
|
13075
|
+
* push would go out through the server that registers it: the push
|
|
13076
|
+
* boundary refuses it (PushHandler). A push naming a channel no server
|
|
13077
|
+
* registers isn't refused, and registers it for the pushing server
|
|
13078
|
+
* (ensureChannelRegistered).
|
|
13079
|
+
*/
|
|
13080
|
+
private channelOfAnotherServer(
|
|
13081
|
+
serverId: string,
|
|
13082
|
+
origin: Record<string, unknown> | undefined,
|
|
13083
|
+
): { channelId: string; holder: string } | undefined {
|
|
13084
|
+
if (!this.channelRegistry) return undefined;
|
|
13085
|
+
const channel = this.derivePushEventChannel(origin);
|
|
13086
|
+
if (!channel) return undefined;
|
|
13087
|
+
let holder: string | undefined;
|
|
13088
|
+
for (const entry of this.channelRegistry.listChannelsRaw()) {
|
|
13089
|
+
if (entry.descriptor.id !== channel.channelId) continue;
|
|
13090
|
+
if (entry.serverId === serverId) return undefined;
|
|
13091
|
+
holder ??= entry.serverId;
|
|
13092
|
+
}
|
|
13093
|
+
return holder === undefined ? undefined : { channelId: channel.channelId, holder };
|
|
13094
|
+
}
|
|
13095
|
+
|
|
12923
13096
|
/**
|
|
12924
13097
|
* Derive the MCPL composite channel id (the outbound routing locus) for a
|
|
12925
13098
|
* push event from its server-defined `origin`, if it carries one.
|
|
@@ -13251,6 +13424,7 @@ export class AgentFramework {
|
|
|
13251
13424
|
suppressProse: silentOnly ? trigger?.suppressProse : undefined,
|
|
13252
13425
|
silentHeartbeat: silentOnly ? trigger?.silentHeartbeat : undefined,
|
|
13253
13426
|
channelId: channelReq?.channelId,
|
|
13427
|
+
serverId: channelReq?.channelId !== undefined ? onlyServerFor(requests, channelReq.channelId) : undefined,
|
|
13254
13428
|
addressed: addressedReq !== undefined,
|
|
13255
13429
|
// A context-budget restart continues the same logical turn: it keeps
|
|
13256
13430
|
// the channel for routing but names no author — the restart is its
|
|
@@ -13718,8 +13892,8 @@ export class AgentFramework {
|
|
|
13718
13892
|
/** The drafts tool's answer to a resident's call, from the model's own
|
|
13719
13893
|
* dispatch or an agent-origin call through executeToolCall (a puppet's):
|
|
13720
13894
|
* a failure the handler throws is answered as one. */
|
|
13721
|
-
private draftsToolResult(agentName: string, rawInput: unknown): Promise<ToolResult> {
|
|
13722
|
-
return this.handleDraftsTool(agentName, rawInput).catch((err): ToolResult => ({
|
|
13895
|
+
private draftsToolResult(agentName: string, rawInput: unknown, callerTurnId?: number): Promise<ToolResult> {
|
|
13896
|
+
return this.handleDraftsTool(agentName, rawInput, callerTurnId).catch((err): ToolResult => ({
|
|
13723
13897
|
success: false,
|
|
13724
13898
|
error: `drafts failed: ${thrownText(err)}`,
|
|
13725
13899
|
isError: true,
|
|
@@ -13732,7 +13906,7 @@ export class AgentFramework {
|
|
|
13732
13906
|
* each draft is published verbatim, in order, through the normal publish
|
|
13733
13907
|
* executor, its attempt journaled durably before the request leaves.
|
|
13734
13908
|
*/
|
|
13735
|
-
private async handleDraftsTool(agentName: string, rawInput: unknown): Promise<ToolResult> {
|
|
13909
|
+
private async handleDraftsTool(agentName: string, rawInput: unknown, callerTurnId?: number): Promise<ToolResult> {
|
|
13736
13910
|
const input = (rawInput && typeof rawInput === 'object' && !Array.isArray(rawInput) ? rawInput : {}) as Record<string, unknown>;
|
|
13737
13911
|
// The turn that sends: a resent draft's channel is recorded as engaged on
|
|
13738
13912
|
// it, as a send tool's is, even when the result comes after the turn.
|
|
@@ -13899,7 +14073,17 @@ export class AgentFramework {
|
|
|
13899
14073
|
const cand = resolved.candidates?.length ? ` It could mean: ${resolved.candidates.join(', ')}.` : '';
|
|
13900
14074
|
return refuse(`Destination "${spec}" did not resolve: ${resolved.error}.${cand} Nothing was sent.`);
|
|
13901
14075
|
}
|
|
13902
|
-
|
|
14076
|
+
// A resend into a channel the calling turn recorded a server for goes
|
|
14077
|
+
// through that server (triggerServerFor), where the call was made inside
|
|
14078
|
+
// that turn; a background script's call carries no turn.
|
|
14079
|
+
const callingRouting = this.turnRouting.get(agentName);
|
|
14080
|
+
const triggerServer = callerTurnId !== undefined && callingRouting?.turnId === callerTurnId
|
|
14081
|
+
? triggerServerFor(callingRouting, resolved.channelId)
|
|
14082
|
+
: undefined;
|
|
14083
|
+
const target = registry.resolveDestination({
|
|
14084
|
+
...(triggerServer !== undefined ? { serverId: triggerServer } : {}),
|
|
14085
|
+
channelId: resolved.channelId,
|
|
14086
|
+
});
|
|
13903
14087
|
if ('error' in target) return refuse(`Destination "${spec}": ${target.error}. Nothing was sent.`);
|
|
13904
14088
|
const destination = target.destination;
|
|
13905
14089
|
if (home && destination.channelId !== home) {
|
|
@@ -14252,7 +14436,10 @@ export class AgentFramework {
|
|
|
14252
14436
|
if (!open) return;
|
|
14253
14437
|
if ((metadata as { tuneOut?: unknown } | undefined)?.tuneOut) return;
|
|
14254
14438
|
const channelId = this.repinChannel(metadata, open);
|
|
14255
|
-
if (channelId)
|
|
14439
|
+
if (channelId) {
|
|
14440
|
+
const serverId = messageServerId(metadata);
|
|
14441
|
+
open.arrivals.push({ messageId, channelId, ...(serverId !== undefined ? { serverId } : {}) });
|
|
14442
|
+
}
|
|
14256
14443
|
}
|
|
14257
14444
|
|
|
14258
14445
|
/**
|
|
@@ -14288,14 +14475,14 @@ export class AgentFramework {
|
|
|
14288
14475
|
* message in the store; the last compile can't be the test, since an
|
|
14289
14476
|
* arrival between the turn's streams isn't in it yet.
|
|
14290
14477
|
*/
|
|
14291
|
-
private remainingArrival(agent: Agent, routing: TurnRouting):
|
|
14478
|
+
private remainingArrival(agent: Agent, routing: TurnRouting): Arrival | undefined {
|
|
14292
14479
|
const cm = agent.getContextManager();
|
|
14293
14480
|
for (let i = routing.arrivals.length - 1; i >= 0; i--) {
|
|
14294
|
-
const
|
|
14481
|
+
const arrival = routing.arrivals[i]!;
|
|
14295
14482
|
// The store answers null for a message that isn't there. A store that
|
|
14296
14483
|
// can't be read throws here, as anywhere else in the start, rather
|
|
14297
14484
|
// than have its candidates read as withdrawn.
|
|
14298
|
-
if (cm.getMessage(messageId)) return
|
|
14485
|
+
if (cm.getMessage(arrival.messageId)) return arrival;
|
|
14299
14486
|
}
|
|
14300
14487
|
return undefined;
|
|
14301
14488
|
}
|
|
@@ -14317,10 +14504,12 @@ export class AgentFramework {
|
|
|
14317
14504
|
routing: TurnRouting,
|
|
14318
14505
|
channelId: string,
|
|
14319
14506
|
cause: string,
|
|
14507
|
+
/** The server of the message that moved it. */
|
|
14508
|
+
serverId?: string,
|
|
14320
14509
|
): { participant: string; content: ContentBlock[]; metadata: MessageMetadata } | undefined {
|
|
14321
14510
|
if (routing.locus === channelId) return undefined;
|
|
14322
14511
|
const prevPin = routing.locus ?? null;
|
|
14323
|
-
routing
|
|
14512
|
+
setTurnLocus(routing, channelId, serverId);
|
|
14324
14513
|
const label = this.channelRegistry?.getDescriptor(channelId)?.label;
|
|
14325
14514
|
const shown = label && label !== channelId
|
|
14326
14515
|
? `${label.startsWith('#') ? label : `#${label}`} (${channelId})`
|
|
@@ -14606,11 +14795,13 @@ export class AgentFramework {
|
|
|
14606
14795
|
body: string,
|
|
14607
14796
|
channelId: string,
|
|
14608
14797
|
unsent: Draft | undefined,
|
|
14798
|
+
/** The server the channel resolves with (triggerServerFor). */
|
|
14799
|
+
serverId: string | undefined,
|
|
14609
14800
|
): Promise<PublishOutcome> {
|
|
14610
14801
|
const registry = this.channelRegistry!;
|
|
14611
14802
|
let attemptId: string | undefined;
|
|
14612
14803
|
if (unsent) {
|
|
14613
|
-
const target = registry.resolveDestination({ channelId });
|
|
14804
|
+
const target = registry.resolveDestination({ ...(serverId !== undefined ? { serverId } : {}), channelId });
|
|
14614
14805
|
if (!('error' in target)) {
|
|
14615
14806
|
try {
|
|
14616
14807
|
attemptId = this.proseDrafts.beginAttempt(agent.name, unsent.id, target.destination, 'unsent-token', false);
|
|
@@ -14631,7 +14822,8 @@ export class AgentFramework {
|
|
|
14631
14822
|
try {
|
|
14632
14823
|
// Both callers hold a failed delivery's words as a bounce draft, whose
|
|
14633
14824
|
// notice says what happened, so the registry writes no second marker.
|
|
14634
|
-
outcome = await registry.deliverSpeech(agent.name, body, channelId,
|
|
14825
|
+
outcome = await registry.deliverSpeech(agent.name, body, channelId,
|
|
14826
|
+
{ heldOnFailure: true, ...(serverId !== undefined ? { locusServerId: serverId } : {}) });
|
|
14635
14827
|
} finally {
|
|
14636
14828
|
if (flight) {
|
|
14637
14829
|
this.draftsInFlight.delete(flight);
|
|
@@ -14730,7 +14922,7 @@ export class AgentFramework {
|
|
|
14730
14922
|
continue;
|
|
14731
14923
|
}
|
|
14732
14924
|
try {
|
|
14733
|
-
const outcome = await this.deliverWithUnsent(agent, body, resolved.channelId, unsent.draft);
|
|
14925
|
+
const outcome = await this.deliverWithUnsent(agent, body, resolved.channelId, unsent.draft, triggerServerFor(routing, resolved.channelId));
|
|
14734
14926
|
this.recordProseDelivery(routing, outcome.status === 'delivered' ? { delivered: true, channelId: outcome.destination!.channelId } : null);
|
|
14735
14927
|
if (outcome.status === 'delivered') {
|
|
14736
14928
|
this.proseBounceStreaks.delete(agent.name);
|
|
@@ -14772,7 +14964,7 @@ export class AgentFramework {
|
|
|
14772
14964
|
}
|
|
14773
14965
|
const target = sticky ?? locus;
|
|
14774
14966
|
try {
|
|
14775
|
-
const outcome = await this.channelRegistry!.routeSpeech(agent.name, envelope, target);
|
|
14967
|
+
const outcome = await this.channelRegistry!.routeSpeech(agent.name, envelope, target, speechServerOpts(routing, target));
|
|
14776
14968
|
this.recordProseDelivery(routing, outcome);
|
|
14777
14969
|
} catch (err) {
|
|
14778
14970
|
console.error('hybrid locus delivery failed:', err);
|
|
@@ -14895,7 +15087,7 @@ export class AgentFramework {
|
|
|
14895
15087
|
}
|
|
14896
15088
|
|
|
14897
15089
|
try {
|
|
14898
|
-
const outcome = await this.deliverWithUnsent(agent, body, targetChannel, unsent.draft);
|
|
15090
|
+
const outcome = await this.deliverWithUnsent(agent, body, targetChannel, unsent.draft, triggerServerFor(routing, targetChannel));
|
|
14899
15091
|
if (outcome.status === 'delivered') {
|
|
14900
15092
|
this.proseBounceStreaks.delete(name);
|
|
14901
15093
|
} else if (outcome.status === 'failed') {
|
|
@@ -15611,6 +15803,9 @@ export class AgentFramework {
|
|
|
15611
15803
|
// turn, which falls back to the global default rather than inheriting
|
|
15612
15804
|
// a previous turn's channel.
|
|
15613
15805
|
routing.triggerChannel = trigger?.channelId;
|
|
15806
|
+
routing.triggerOrigin = trigger?.channelId && trigger.serverId
|
|
15807
|
+
? { channelId: trigger.channelId, serverId: trigger.serverId }
|
|
15808
|
+
: undefined;
|
|
15614
15809
|
// FREEZE the turn's outbound locus now (turn-frozen routing): the home
|
|
15615
15810
|
// channel for forks, else the trigger channel, else the global default,
|
|
15616
15811
|
// which an ephemeral run never takes (takesGlobalFallback).
|
|
@@ -15626,7 +15821,14 @@ export class AgentFramework {
|
|
|
15626
15821
|
// stability).
|
|
15627
15822
|
if (turnProseRouting !== 'explicit' && turnProseRouting !== 'disabled') {
|
|
15628
15823
|
const locus = this.channelRegistry?.resolveLocus(agent.name) ?? null;
|
|
15629
|
-
|
|
15824
|
+
// A locus that is neither the trigger's channel nor the agent's home
|
|
15825
|
+
// is the global fallback: its server is the latest accepted
|
|
15826
|
+
// message's, taken now, with the locus.
|
|
15827
|
+
if (locus !== null) {
|
|
15828
|
+
setTurnLocus(routing, locus, locus !== routing.triggerChannel && !this.conversationAgentHomes.has(agent.name)
|
|
15829
|
+
? this.channelRegistry?.fallbackServerFor(locus)
|
|
15830
|
+
: undefined);
|
|
15831
|
+
}
|
|
15630
15832
|
if (attempt === 0) this.announceLocusIfChanged(agent.name, locus);
|
|
15631
15833
|
}
|
|
15632
15834
|
} else {
|
|
@@ -15639,7 +15841,7 @@ export class AgentFramework {
|
|
|
15639
15841
|
? this.remainingArrival(agent, routing)
|
|
15640
15842
|
: undefined;
|
|
15641
15843
|
routing.arrivals = [];
|
|
15642
|
-
if (arrival) this.repinTurnLocus(agent, routing, arrival, 'arrival');
|
|
15844
|
+
if (arrival) this.repinTurnLocus(agent, routing, arrival.channelId, 'arrival', arrival.serverId);
|
|
15643
15845
|
}
|
|
15644
15846
|
// A silent start, or a disabled agent's, has no locus to show, whatever a
|
|
15645
15847
|
// channel_open in a silent turn left on its object.
|
|
@@ -16192,7 +16394,7 @@ export class AgentFramework {
|
|
|
16192
16394
|
};
|
|
16193
16395
|
const enqueueSpeech = (text: string, locus: string | null): void => {
|
|
16194
16396
|
chainSpeech(async () => {
|
|
16195
|
-
const outcome = await this.channelRegistry!.routeSpeech(agent.name, text, locus);
|
|
16397
|
+
const outcome = await this.channelRegistry!.routeSpeech(agent.name, text, locus, speechServerOpts(streamRouting, locus));
|
|
16196
16398
|
this.recordProseDelivery(streamRouting, outcome);
|
|
16197
16399
|
}, 'mid-turn speech routing failed:');
|
|
16198
16400
|
};
|
|
@@ -16315,6 +16517,7 @@ export class AgentFramework {
|
|
|
16315
16517
|
for (const rd of deltas) {
|
|
16316
16518
|
this.channelRegistry!.sendOutgoingChunk(
|
|
16317
16519
|
rd.channelId, agent.name, outgoingInferenceId, outgoingIndex++, rd.delta,
|
|
16520
|
+
triggerServerFor(streamRouting, rd.channelId),
|
|
16318
16521
|
);
|
|
16319
16522
|
}
|
|
16320
16523
|
};
|
|
@@ -17237,7 +17440,7 @@ export class AgentFramework {
|
|
|
17237
17440
|
);
|
|
17238
17441
|
for (const segment of speechSegments) {
|
|
17239
17442
|
try {
|
|
17240
|
-
const outcome = await this.channelRegistry.routeSpeech(agent.name, segment, locus);
|
|
17443
|
+
const outcome = await this.channelRegistry.routeSpeech(agent.name, segment, locus, speechServerOpts(streamRouting, locus));
|
|
17241
17444
|
this.recordProseDelivery(streamRouting, outcome);
|
|
17242
17445
|
} catch (err) {
|
|
17243
17446
|
console.error('speech routing failed:', err);
|
|
@@ -17334,7 +17537,7 @@ export class AgentFramework {
|
|
|
17334
17537
|
// Deliver sequentially (await each) so the segments land in order.
|
|
17335
17538
|
for (const seg of segments) {
|
|
17336
17539
|
try {
|
|
17337
|
-
const outcome = await this.channelRegistry.routeSpeech(agent.name, seg, locus);
|
|
17540
|
+
const outcome = await this.channelRegistry.routeSpeech(agent.name, seg, locus, speechServerOpts(streamRouting, locus));
|
|
17338
17541
|
this.recordProseDelivery(streamRouting, outcome);
|
|
17339
17542
|
} catch (err) {
|
|
17340
17543
|
console.error('speech routing failed:', err);
|
|
@@ -17992,7 +18195,7 @@ export class AgentFramework {
|
|
|
17992
18195
|
if (frameReachedTerminal && proseStream) {
|
|
17993
18196
|
emitOutgoing(proseStream.finish());
|
|
17994
18197
|
for (const [channelId, text] of proseStream.byChannel()) {
|
|
17995
|
-
this.channelRegistry!.sendOutgoingComplete(channelId, agent.name, outgoingInferenceId, text);
|
|
18198
|
+
this.channelRegistry!.sendOutgoingComplete(channelId, agent.name, outgoingInferenceId, text, triggerServerFor(streamRouting, channelId));
|
|
17996
18199
|
}
|
|
17997
18200
|
}
|
|
17998
18201
|
this.frameworkCancelledStreams.delete(`${agent.name}:${myStreamId}`);
|
|
@@ -18296,7 +18499,7 @@ export class AgentFramework {
|
|
|
18296
18499
|
// agent origin and acts as that resident, as the model's own call does
|
|
18297
18500
|
// through dispatchToolCall. A module holds no drafts.
|
|
18298
18501
|
if (call.name === 'drafts' && this.channelRegistry && origin.kind === 'agent') {
|
|
18299
|
-
return this.draftsToolResult(origin.agentName, call.input);
|
|
18502
|
+
return this.draftsToolResult(origin.agentName, call.input, call.callerTurnId);
|
|
18300
18503
|
}
|
|
18301
18504
|
|
|
18302
18505
|
// The utils meta-tool — programmatic callers (code_execution scripts,
|
|
@@ -19891,7 +20094,7 @@ export class AgentFramework {
|
|
|
19891
20094
|
if (enrichedCall.name === 'drafts' && this.channelRegistry) {
|
|
19892
20095
|
const startedAt = Date.now();
|
|
19893
20096
|
this.emitTrace({ type: 'tool:started', module: 'framework', tool: 'drafts', callId: enrichedCall.id, input: enrichedCall.input });
|
|
19894
|
-
void this.draftsToolResult(agentName, enrichedCall.input)
|
|
20097
|
+
void this.draftsToolResult(agentName, enrichedCall.input, enrichedCall.callerTurnId)
|
|
19895
20098
|
.then((result) => {
|
|
19896
20099
|
this.emitTrace({ type: 'tool:completed', module: 'framework', tool: 'drafts', callId: enrichedCall.id, durationMs: Date.now() - startedAt });
|
|
19897
20100
|
this.pushEvent({ type: 'tool-result', callId: enrichedCall.id, toolName: enrichedCall.name, agentName, moduleName: 'framework', result });
|
|
@@ -21440,6 +21643,7 @@ export class AgentFramework {
|
|
|
21440
21643
|
this.noteInboundAccepted(source);
|
|
21441
21644
|
return source;
|
|
21442
21645
|
},
|
|
21646
|
+
(serverId, params) => this.channelOfAnotherServer(serverId, params.origin),
|
|
21443
21647
|
);
|
|
21444
21648
|
|
|
21445
21649
|
// Server-initiated inference router (Step 6)
|
|
@@ -25075,8 +25279,17 @@ export class AgentFramework {
|
|
|
25075
25279
|
// ordinary wake, starts only once its over-budget boundary has every
|
|
25076
25280
|
// result of the round.
|
|
25077
25281
|
const callingTurn = this.routingOf(agentName);
|
|
25282
|
+
const inTurn = call.callerTurnId !== undefined && call.callerTurnId === callingTurn.turnId;
|
|
25078
25283
|
|
|
25079
|
-
this.channelRegistry!.handleChannelToolCall(call.name, call.input, {
|
|
25284
|
+
this.channelRegistry!.handleChannelToolCall(call.name, call.input, {
|
|
25285
|
+
kind: 'agent',
|
|
25286
|
+
agentName,
|
|
25287
|
+
// The turn's recorded servers go only with a call made inside that
|
|
25288
|
+
// turn: a background script's call carries no turn, and takes a bare
|
|
25289
|
+
// id only where exactly one server registers it.
|
|
25290
|
+
...(inTurn && callingTurn.triggerOrigin !== undefined ? { trigger: callingTurn.triggerOrigin } : {}),
|
|
25291
|
+
...(inTurn && callingTurn.locusOrigin !== undefined ? { locus: callingTurn.locusOrigin } : {}),
|
|
25292
|
+
})
|
|
25080
25293
|
.then((result) => {
|
|
25081
25294
|
const durationMs = Date.now() - startTime;
|
|
25082
25295
|
this.emitTrace({
|
|
@@ -25114,7 +25327,13 @@ export class AgentFramework {
|
|
|
25114
25327
|
if (openerAgent
|
|
25115
25328
|
&& (openerAgent.proseRouting === 'locus' || openerAgent.proseRouting === 'hybrid')
|
|
25116
25329
|
&& this.openLogical.get(agentName) === callingTurn) {
|
|
25117
|
-
|
|
25330
|
+
const openedOn = this.channelRegistry!.resolveDestination({
|
|
25331
|
+
...(typeof (call.input as { serverId?: unknown } | undefined)?.serverId === 'string'
|
|
25332
|
+
? { serverId: (call.input as { serverId: string }).serverId }
|
|
25333
|
+
: {}),
|
|
25334
|
+
channelId: opened,
|
|
25335
|
+
});
|
|
25336
|
+
setTurnLocus(callingTurn, opened, 'error' in openedOn ? undefined : openedOn.destination.serverId);
|
|
25118
25337
|
this.lastAnnouncedLocus.set(agentName, opened);
|
|
25119
25338
|
result = {
|
|
25120
25339
|
...result,
|
|
@@ -25388,17 +25607,21 @@ export class AgentFramework {
|
|
|
25388
25607
|
} else if (agent?.proseRouting === 'explicit') {
|
|
25389
25608
|
// Explicit mode: announce to the turn's sticky prose target if the
|
|
25390
25609
|
// model has set one; otherwise stay quiet — never guess a channel.
|
|
25391
|
-
const
|
|
25610
|
+
const routing = this.turnRouting.get(agentName);
|
|
25611
|
+
const target = routing?.pin;
|
|
25392
25612
|
if (target) {
|
|
25393
|
-
this.channelRegistry.routeSpeech(agentName, text, target
|
|
25613
|
+
this.channelRegistry.routeSpeech(agentName, text, target,
|
|
25614
|
+
speechServerOpts(call.callerTurnId === routing?.turnId ? routing : undefined, target)).catch((err) => {
|
|
25394
25615
|
console.error('[sleep] announce failed:', err instanceof Error ? err.message : err);
|
|
25395
25616
|
});
|
|
25396
25617
|
} else {
|
|
25397
25618
|
console.error(`[sleep] ${agentName}: no prose target this turn — sleep announcement not posted (explicit mode never guesses)`);
|
|
25398
25619
|
}
|
|
25399
25620
|
} else {
|
|
25400
|
-
const
|
|
25401
|
-
|
|
25621
|
+
const routing = this.turnRouting.get(agentName);
|
|
25622
|
+
const locus = routing?.locus ?? null;
|
|
25623
|
+
this.channelRegistry.routeSpeech(agentName, text, locus,
|
|
25624
|
+
speechServerOpts(call.callerTurnId === routing?.turnId ? routing : undefined, locus)).catch((err) => {
|
|
25402
25625
|
console.error('[sleep] announce failed:', err instanceof Error ? err.message : err);
|
|
25403
25626
|
});
|
|
25404
25627
|
}
|