@animalabs/agent-framework 0.23.2 → 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 +15 -1
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +191 -55
- 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 +219 -47
- package/src/gate/event-gate.ts +6 -1
- package/src/mcpl/channel-registry.ts +106 -24
- package/src/mcpl/conversation-router.ts +65 -14
- package/src/mcpl/push-handler.ts +52 -2
- package/src/types/agent.ts +6 -0
package/dist/src/framework.js
CHANGED
|
@@ -523,8 +523,55 @@ function announcedDomainsOf(announce) {
|
|
|
523
523
|
function resumesLogicalTurn(reason) {
|
|
524
524
|
return reason === 'context_budget_restart' || reason === 'tool_result_guard_retry';
|
|
525
525
|
}
|
|
526
|
+
/** The server a stored channel message came from, as the host wrote it. */
|
|
527
|
+
function messageServerId(metadata) {
|
|
528
|
+
const serverId = metadata?.serverId;
|
|
529
|
+
return typeof serverId === 'string' && serverId ? serverId : undefined;
|
|
530
|
+
}
|
|
526
531
|
/** The last TurnRouting.turnId given out: ids increase, and are unique within the process. */
|
|
527
532
|
let lastTurnId = 0;
|
|
533
|
+
/**
|
|
534
|
+
* The server every request naming `channelId` came from, or undefined where
|
|
535
|
+
* one names no server or they name more than one (TurnRouting.triggerOrigin).
|
|
536
|
+
*/
|
|
537
|
+
function onlyServerFor(requests, channelId) {
|
|
538
|
+
let serverId;
|
|
539
|
+
for (const r of requests) {
|
|
540
|
+
if (r.channelId !== channelId)
|
|
541
|
+
continue;
|
|
542
|
+
if (!r.serverId || (serverId !== undefined && r.serverId !== serverId))
|
|
543
|
+
return undefined;
|
|
544
|
+
serverId = r.serverId;
|
|
545
|
+
}
|
|
546
|
+
return serverId;
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* The server a send in `routing`'s turn resolves `channelId` with: the
|
|
550
|
+
* trigger's, where `channelId` is the trigger's channel
|
|
551
|
+
* (TurnRouting.triggerOrigin), else the locus's, where it's the channel the
|
|
552
|
+
* locus's server put there (TurnRouting.locusOrigin).
|
|
553
|
+
*/
|
|
554
|
+
function triggerServerFor(routing, channelId) {
|
|
555
|
+
if (routing === undefined || !channelId)
|
|
556
|
+
return undefined;
|
|
557
|
+
if (routing.triggerOrigin?.channelId === channelId)
|
|
558
|
+
return routing.triggerOrigin.serverId;
|
|
559
|
+
if (routing.locusOrigin?.channelId === channelId)
|
|
560
|
+
return routing.locusOrigin.serverId;
|
|
561
|
+
return undefined;
|
|
562
|
+
}
|
|
563
|
+
/** Move a turn's locus, with the server of the message that put it there
|
|
564
|
+
* (TurnRouting.locusOrigin), or none. */
|
|
565
|
+
function setTurnLocus(routing, channelId, serverId) {
|
|
566
|
+
routing.locus = channelId;
|
|
567
|
+
routing.locusOrigin = serverId !== undefined ? { channelId, serverId } : undefined;
|
|
568
|
+
}
|
|
569
|
+
/** routeSpeech's options for speech into `channelId` in `routing`'s turn:
|
|
570
|
+
* the trigger's server, where it's the trigger's channel (triggerServerFor). */
|
|
571
|
+
function speechServerOpts(routing, channelId) {
|
|
572
|
+
const serverId = triggerServerFor(routing, channelId);
|
|
573
|
+
return serverId !== undefined ? { locusServerId: serverId } : undefined;
|
|
574
|
+
}
|
|
528
575
|
function freshTurnRouting() {
|
|
529
576
|
return {
|
|
530
577
|
turnId: ++lastTurnId,
|
|
@@ -2150,8 +2197,8 @@ export class AgentFramework {
|
|
|
2150
2197
|
// route applies to a fork only when it IS that home, and never to
|
|
2151
2198
|
// another agent when a fork owns that channel. Otherwise a fork
|
|
2152
2199
|
// bound elsewhere would show typing where it isn't replying.
|
|
2153
|
-
...(provenance?.routeChannelId && framework.mayTakeGateRoute(agentName, provenance.routeChannelId)
|
|
2154
|
-
? { channelId: provenance.routeChannelId, addressed: true }
|
|
2200
|
+
...(provenance?.routeChannelId && framework.mayTakeGateRoute(agentName, provenance.routeChannelId, provenance.routeServerId)
|
|
2201
|
+
? { channelId: provenance.routeChannelId, serverId: provenance.routeServerId, addressed: true }
|
|
2155
2202
|
: {}),
|
|
2156
2203
|
});
|
|
2157
2204
|
},
|
|
@@ -7451,7 +7498,9 @@ export class AgentFramework {
|
|
|
7451
7498
|
// tools/call directly on its connection (bare tool name — no prefix
|
|
7452
7499
|
// games), bypassing the agent event queue so no synthetic tool-result
|
|
7453
7500
|
// is injected into a turn the agent never took.
|
|
7454
|
-
|
|
7501
|
+
// Through the server whose message it marks: the latest accepted
|
|
7502
|
+
// message's, kept beside its channel and id (fallbackServerFor).
|
|
7503
|
+
const serverId = this.channelRegistry?.fallbackServerFor(incoming.channelId);
|
|
7455
7504
|
const server = serverId ? this.mcplServerRegistry?.getServer(serverId) : null;
|
|
7456
7505
|
if (!server) {
|
|
7457
7506
|
console.error(`[inference-refusal] reaction skipped: no MCPL server for locus ${logValue(incoming.channelId)} (agent=${agentName})`);
|
|
@@ -9685,13 +9734,19 @@ export class AgentFramework {
|
|
|
9685
9734
|
// The last injection that qualifies wins (repinChannel).
|
|
9686
9735
|
const routing = this.routingOf(agent.name);
|
|
9687
9736
|
let newLocus;
|
|
9688
|
-
|
|
9689
|
-
|
|
9737
|
+
let newLocusServer;
|
|
9738
|
+
for (const inj of midTurnInjections) {
|
|
9739
|
+
const channelId = this.repinChannel(inj.metadata, routing);
|
|
9740
|
+
if (channelId) {
|
|
9741
|
+
newLocus = channelId;
|
|
9742
|
+
newLocusServer = messageServerId(inj.metadata);
|
|
9743
|
+
}
|
|
9744
|
+
}
|
|
9690
9745
|
if (newLocus) {
|
|
9691
9746
|
// What this boundary just stored is newer than any arrival the
|
|
9692
9747
|
// turn holds, so the arrivals can't move the locus back.
|
|
9693
9748
|
routing.arrivals = [];
|
|
9694
|
-
const notice = this.repinTurnLocus(agent, routing, newLocus, 'mid-turn addressed');
|
|
9749
|
+
const notice = this.repinTurnLocus(agent, routing, newLocus, 'mid-turn addressed', newLocusServer);
|
|
9695
9750
|
if (notice)
|
|
9696
9751
|
midTurnInjections.push(notice);
|
|
9697
9752
|
}
|
|
@@ -10028,14 +10083,15 @@ export class AgentFramework {
|
|
|
10028
10083
|
const id = target.getContextManager().addMessage('user', event.content, metadata);
|
|
10029
10084
|
// A correction may target an older engagement. Refresh only the
|
|
10030
10085
|
// binding whose agent actually received the message.
|
|
10031
|
-
|
|
10032
|
-
|
|
10086
|
+
const channel = { serverId: event.serverId, channelId: event.channelId };
|
|
10087
|
+
if (this.conversationRouter.getBinding(channel)?.agentName === target.name) {
|
|
10088
|
+
this.conversationRouter.touch(channel);
|
|
10033
10089
|
}
|
|
10034
10090
|
this.emitTrace({ type: 'message:added', messageId: id, source: 'mcpl:channel-incoming' });
|
|
10035
10091
|
if (event.triggerInference) {
|
|
10036
10092
|
this.pendingRequests.push({
|
|
10037
10093
|
agentName: target.name, reason: 'mcpl:channel-incoming', source: event.serverId, timestamp: Date.now(),
|
|
10038
|
-
channelId: event.channelId,
|
|
10094
|
+
channelId: event.channelId, serverId: event.serverId,
|
|
10039
10095
|
counterparty: event.author?.id ? `${event.serverId}:user:${event.author.id}` : undefined,
|
|
10040
10096
|
addressed: isAddressedMessage(event.tags, event.metadata),
|
|
10041
10097
|
...(event.coalescingSubject ? { coalescingSubject: event.coalescingSubject } : {}),
|
|
@@ -10107,8 +10163,10 @@ export class AgentFramework {
|
|
|
10107
10163
|
source: event.serverId,
|
|
10108
10164
|
timestamp: Date.now(),
|
|
10109
10165
|
// Route this turn's auto-published speech back to THIS channel, not
|
|
10110
|
-
// the global most-recent-inbound locus (item-3 redux, trunk agents)
|
|
10166
|
+
// the global most-recent-inbound locus (item-3 redux, trunk agents),
|
|
10167
|
+
// through the server that sent it.
|
|
10111
10168
|
channelId: event.channelId,
|
|
10169
|
+
serverId: event.serverId,
|
|
10112
10170
|
counterparty: event.author?.id ? `${event.serverId}:user:${event.author.id}` : undefined,
|
|
10113
10171
|
// Addressed messages outrank ambient chatter when a batched wake
|
|
10114
10172
|
// picks the turn's frozen speech locus.
|
|
@@ -10171,7 +10229,7 @@ export class AgentFramework {
|
|
|
10171
10229
|
if (!agent) {
|
|
10172
10230
|
// Bound agent vanished (e.g. external reset) — unbind so the next
|
|
10173
10231
|
// qualifying message respawns a fresh fork.
|
|
10174
|
-
router.unbind(event.channelId);
|
|
10232
|
+
router.unbind({ serverId: event.serverId, channelId: event.channelId });
|
|
10175
10233
|
this.emitTrace({
|
|
10176
10234
|
type: 'mcpl:conversation-binding-orphaned',
|
|
10177
10235
|
channelId: event.channelId,
|
|
@@ -10185,7 +10243,7 @@ export class AgentFramework {
|
|
|
10185
10243
|
const trigger = decision.trigger && event.triggerInference !== false;
|
|
10186
10244
|
messageMetadata.triggered = trigger;
|
|
10187
10245
|
const id = agent.getContextManager().addMessage('user', event.content, messageMetadata);
|
|
10188
|
-
router.touch(event.channelId);
|
|
10246
|
+
router.touch({ serverId: event.serverId, channelId: event.channelId });
|
|
10189
10247
|
this.emitTrace({ type: 'message:added', messageId: id, source: 'mcpl:channel-incoming' });
|
|
10190
10248
|
if (trigger) {
|
|
10191
10249
|
this.pendingRequests.push({
|
|
@@ -10196,6 +10254,7 @@ export class AgentFramework {
|
|
|
10196
10254
|
// A fork's home channel wins in routeSpeech regardless, but carry the
|
|
10197
10255
|
// triggering channel too so the trunk/active path stays consistent.
|
|
10198
10256
|
channelId: event.channelId,
|
|
10257
|
+
serverId: event.serverId,
|
|
10199
10258
|
counterparty: event.author?.id ? `${event.serverId}:user:${event.author.id}` : undefined,
|
|
10200
10259
|
addressed: isAddressedMessage(event.tags, event.metadata),
|
|
10201
10260
|
...(event.coalescingSubject ? { coalescingSubject: event.coalescingSubject } : {}),
|
|
@@ -10334,7 +10393,7 @@ export class AgentFramework {
|
|
|
10334
10393
|
const decision = this.conversationRouter.route(facts);
|
|
10335
10394
|
if (decision.kind !== 'spawn')
|
|
10336
10395
|
return decision;
|
|
10337
|
-
return { ...decision, spawned: this.spawnConversationAgent(facts
|
|
10396
|
+
return { ...decision, spawned: this.spawnConversationAgent(facts, decision) };
|
|
10338
10397
|
}
|
|
10339
10398
|
}
|
|
10340
10399
|
/**
|
|
@@ -10349,11 +10408,12 @@ export class AgentFramework {
|
|
|
10349
10408
|
* so the entry it removes is its own. It's removed before anyone waiting
|
|
10350
10409
|
* on it resumes, so a waiter that decides again doesn't find it.
|
|
10351
10410
|
*/
|
|
10352
|
-
spawnConversationAgent(
|
|
10411
|
+
spawnConversationAgent(facts, decision) {
|
|
10412
|
+
const channelId = facts.channelId;
|
|
10353
10413
|
const spawn = (async () => {
|
|
10354
10414
|
this.storeConversationGeneration(channelId, decision.generation);
|
|
10355
10415
|
const agent = await this.createConversationAgent(decision.agentName, channelId);
|
|
10356
|
-
this.conversationRouter.bind(channelId, decision.agentName, decision.generation);
|
|
10416
|
+
this.conversationRouter.bind({ serverId: facts.serverId, channelId }, decision.agentName, decision.generation);
|
|
10357
10417
|
return agent;
|
|
10358
10418
|
})();
|
|
10359
10419
|
const done = () => { this.conversationSpawns.delete(channelId); };
|
|
@@ -10476,7 +10536,7 @@ export class AgentFramework {
|
|
|
10476
10536
|
return;
|
|
10477
10537
|
this.lastConversationSweep = now;
|
|
10478
10538
|
for (const binding of this.conversationRouter.expired(now)) {
|
|
10479
|
-
this.conversationRouter.unbind(binding
|
|
10539
|
+
this.conversationRouter.unbind(binding);
|
|
10480
10540
|
this.emitTrace({
|
|
10481
10541
|
type: 'mcpl:conversation-closed',
|
|
10482
10542
|
channelId: binding.channelId,
|
|
@@ -11124,7 +11184,7 @@ export class AgentFramework {
|
|
|
11124
11184
|
return explicit.filter((n) => this.agents.has(n));
|
|
11125
11185
|
if (occ.scope.kind === 'channel' && this.conversationRouter) {
|
|
11126
11186
|
const router = this.conversationRouter;
|
|
11127
|
-
const bound = router.getBinding(occ.scope.id)?.agentName;
|
|
11187
|
+
const bound = router.getBinding({ serverId: occ.serverId, channelId: occ.scope.id })?.agentName;
|
|
11128
11188
|
if (bound)
|
|
11129
11189
|
return this.agents.has(bound) ? [bound] : [];
|
|
11130
11190
|
// No fork yet: the same bind decision an incoming message would get. A
|
|
@@ -11236,6 +11296,7 @@ export class AgentFramework {
|
|
|
11236
11296
|
source: occ.serverId,
|
|
11237
11297
|
timestamp: Date.now(),
|
|
11238
11298
|
channelId,
|
|
11299
|
+
serverId: channelId !== undefined ? occ.serverId : undefined,
|
|
11239
11300
|
counterparty: authorId ? `${occ.serverId}:user:${authorId}` : undefined,
|
|
11240
11301
|
addressed: isAddressedMessage(occ.tags, origin),
|
|
11241
11302
|
...(plain ? {} : { coalescingSubject: subject, coalescingBatch: true }),
|
|
@@ -11453,6 +11514,7 @@ export class AgentFramework {
|
|
|
11453
11514
|
source: event.serverId,
|
|
11454
11515
|
timestamp: Date.now(),
|
|
11455
11516
|
channelId: triggerChannel?.channelId,
|
|
11517
|
+
serverId: triggerChannel ? event.serverId : undefined,
|
|
11456
11518
|
// DMs and addressed-while-closed messages arrive as push events;
|
|
11457
11519
|
// they must outrank ambient chatter in a batched wake's locus
|
|
11458
11520
|
// selection just like their channels/incoming counterparts.
|
|
@@ -11476,18 +11538,49 @@ export class AgentFramework {
|
|
|
11476
11538
|
* Whether a batched gate wake's addressed route may become `agentName`'s
|
|
11477
11539
|
* speech locus. The gate broadcasts to every agent, conversation forks
|
|
11478
11540
|
* included. A fork speaks only in its home channel, and a channel bound to
|
|
11479
|
-
* a fork is that fork's conversation, so no other agent takes it.
|
|
11541
|
+
* a fork is that fork's conversation, so no other agent takes it. Two
|
|
11542
|
+
* servers' channels with one id are two conversations, each bound to its
|
|
11543
|
+
* own fork with that id as its home, so a fork takes the route only where
|
|
11544
|
+
* the router binds the route's channel, its server and id, to it; a route
|
|
11545
|
+
* with no server names a fork only where one fork is bound to that id.
|
|
11480
11546
|
*/
|
|
11481
|
-
mayTakeGateRoute(agentName, routeChannelId) {
|
|
11547
|
+
mayTakeGateRoute(agentName, routeChannelId, routeServerId) {
|
|
11482
11548
|
const home = this.conversationAgentHomes.get(agentName);
|
|
11483
|
-
if (home !== undefined)
|
|
11484
|
-
return home === routeChannelId
|
|
11549
|
+
if (home !== undefined) {
|
|
11550
|
+
return home === routeChannelId
|
|
11551
|
+
&& this.conversationRouter?.getBinding({ serverId: routeServerId, channelId: routeChannelId })?.agentName === agentName;
|
|
11552
|
+
}
|
|
11485
11553
|
for (const boundChannel of this.conversationAgentHomes.values()) {
|
|
11486
11554
|
if (boundChannel === routeChannelId)
|
|
11487
11555
|
return false;
|
|
11488
11556
|
}
|
|
11489
11557
|
return true;
|
|
11490
11558
|
}
|
|
11559
|
+
/**
|
|
11560
|
+
* The channel a push names, and a server that registers it, when another
|
|
11561
|
+
* server registers that channel id and the pushing server doesn't. A
|
|
11562
|
+
* channel's id is the agent's locus on its own, so the reply to such a
|
|
11563
|
+
* push would go out through the server that registers it: the push
|
|
11564
|
+
* boundary refuses it (PushHandler). A push naming a channel no server
|
|
11565
|
+
* registers isn't refused, and registers it for the pushing server
|
|
11566
|
+
* (ensureChannelRegistered).
|
|
11567
|
+
*/
|
|
11568
|
+
channelOfAnotherServer(serverId, origin) {
|
|
11569
|
+
if (!this.channelRegistry)
|
|
11570
|
+
return undefined;
|
|
11571
|
+
const channel = this.derivePushEventChannel(origin);
|
|
11572
|
+
if (!channel)
|
|
11573
|
+
return undefined;
|
|
11574
|
+
let holder;
|
|
11575
|
+
for (const entry of this.channelRegistry.listChannelsRaw()) {
|
|
11576
|
+
if (entry.descriptor.id !== channel.channelId)
|
|
11577
|
+
continue;
|
|
11578
|
+
if (entry.serverId === serverId)
|
|
11579
|
+
return undefined;
|
|
11580
|
+
holder ??= entry.serverId;
|
|
11581
|
+
}
|
|
11582
|
+
return holder === undefined ? undefined : { channelId: channel.channelId, holder };
|
|
11583
|
+
}
|
|
11491
11584
|
/**
|
|
11492
11585
|
* Derive the MCPL composite channel id (the outbound routing locus) for a
|
|
11493
11586
|
* push event from its server-defined `origin`, if it carries one.
|
|
@@ -11806,6 +11899,7 @@ export class AgentFramework {
|
|
|
11806
11899
|
suppressProse: silentOnly ? trigger?.suppressProse : undefined,
|
|
11807
11900
|
silentHeartbeat: silentOnly ? trigger?.silentHeartbeat : undefined,
|
|
11808
11901
|
channelId: channelReq?.channelId,
|
|
11902
|
+
serverId: channelReq?.channelId !== undefined ? onlyServerFor(requests, channelReq.channelId) : undefined,
|
|
11809
11903
|
addressed: addressedReq !== undefined,
|
|
11810
11904
|
// A context-budget restart continues the same logical turn: it keeps
|
|
11811
11905
|
// the channel for routing but names no author — the restart is its
|
|
@@ -12255,8 +12349,8 @@ export class AgentFramework {
|
|
|
12255
12349
|
/** The drafts tool's answer to a resident's call, from the model's own
|
|
12256
12350
|
* dispatch or an agent-origin call through executeToolCall (a puppet's):
|
|
12257
12351
|
* a failure the handler throws is answered as one. */
|
|
12258
|
-
draftsToolResult(agentName, rawInput) {
|
|
12259
|
-
return this.handleDraftsTool(agentName, rawInput).catch((err) => ({
|
|
12352
|
+
draftsToolResult(agentName, rawInput, callerTurnId) {
|
|
12353
|
+
return this.handleDraftsTool(agentName, rawInput, callerTurnId).catch((err) => ({
|
|
12260
12354
|
success: false,
|
|
12261
12355
|
error: `drafts failed: ${thrownText(err)}`,
|
|
12262
12356
|
isError: true,
|
|
@@ -12268,7 +12362,7 @@ export class AgentFramework {
|
|
|
12268
12362
|
* each draft is published verbatim, in order, through the normal publish
|
|
12269
12363
|
* executor, its attempt journaled durably before the request leaves.
|
|
12270
12364
|
*/
|
|
12271
|
-
async handleDraftsTool(agentName, rawInput) {
|
|
12365
|
+
async handleDraftsTool(agentName, rawInput, callerTurnId) {
|
|
12272
12366
|
const input = (rawInput && typeof rawInput === 'object' && !Array.isArray(rawInput) ? rawInput : {});
|
|
12273
12367
|
// The turn that sends: a resent draft's channel is recorded as engaged on
|
|
12274
12368
|
// it, as a send tool's is, even when the result comes after the turn.
|
|
@@ -12439,7 +12533,17 @@ export class AgentFramework {
|
|
|
12439
12533
|
const cand = resolved.candidates?.length ? ` It could mean: ${resolved.candidates.join(', ')}.` : '';
|
|
12440
12534
|
return refuse(`Destination "${spec}" did not resolve: ${resolved.error}.${cand} Nothing was sent.`);
|
|
12441
12535
|
}
|
|
12442
|
-
|
|
12536
|
+
// A resend into a channel the calling turn recorded a server for goes
|
|
12537
|
+
// through that server (triggerServerFor), where the call was made inside
|
|
12538
|
+
// that turn; a background script's call carries no turn.
|
|
12539
|
+
const callingRouting = this.turnRouting.get(agentName);
|
|
12540
|
+
const triggerServer = callerTurnId !== undefined && callingRouting?.turnId === callerTurnId
|
|
12541
|
+
? triggerServerFor(callingRouting, resolved.channelId)
|
|
12542
|
+
: undefined;
|
|
12543
|
+
const target = registry.resolveDestination({
|
|
12544
|
+
...(triggerServer !== undefined ? { serverId: triggerServer } : {}),
|
|
12545
|
+
channelId: resolved.channelId,
|
|
12546
|
+
});
|
|
12443
12547
|
if ('error' in target)
|
|
12444
12548
|
return refuse(`Destination "${spec}": ${target.error}. Nothing was sent.`);
|
|
12445
12549
|
const destination = target.destination;
|
|
@@ -12783,8 +12887,10 @@ export class AgentFramework {
|
|
|
12783
12887
|
if (metadata?.tuneOut)
|
|
12784
12888
|
return;
|
|
12785
12889
|
const channelId = this.repinChannel(metadata, open);
|
|
12786
|
-
if (channelId)
|
|
12787
|
-
|
|
12890
|
+
if (channelId) {
|
|
12891
|
+
const serverId = messageServerId(metadata);
|
|
12892
|
+
open.arrivals.push({ messageId, channelId, ...(serverId !== undefined ? { serverId } : {}) });
|
|
12893
|
+
}
|
|
12788
12894
|
}
|
|
12789
12895
|
/**
|
|
12790
12896
|
* Whether `agent`'s store is the residents' one message slot, which every
|
|
@@ -12821,12 +12927,12 @@ export class AgentFramework {
|
|
|
12821
12927
|
remainingArrival(agent, routing) {
|
|
12822
12928
|
const cm = agent.getContextManager();
|
|
12823
12929
|
for (let i = routing.arrivals.length - 1; i >= 0; i--) {
|
|
12824
|
-
const
|
|
12930
|
+
const arrival = routing.arrivals[i];
|
|
12825
12931
|
// The store answers null for a message that isn't there. A store that
|
|
12826
12932
|
// can't be read throws here, as anywhere else in the start, rather
|
|
12827
12933
|
// than have its candidates read as withdrawn.
|
|
12828
|
-
if (cm.getMessage(messageId))
|
|
12829
|
-
return
|
|
12934
|
+
if (cm.getMessage(arrival.messageId))
|
|
12935
|
+
return arrival;
|
|
12830
12936
|
}
|
|
12831
12937
|
return undefined;
|
|
12832
12938
|
}
|
|
@@ -12842,11 +12948,13 @@ export class AgentFramework {
|
|
|
12842
12948
|
* stored. The locus has moved then too, but the announce baseline hasn't,
|
|
12843
12949
|
* so the agent's next turn announces where its speech lands.
|
|
12844
12950
|
*/
|
|
12845
|
-
repinTurnLocus(agent, routing, channelId, cause
|
|
12951
|
+
repinTurnLocus(agent, routing, channelId, cause,
|
|
12952
|
+
/** The server of the message that moved it. */
|
|
12953
|
+
serverId) {
|
|
12846
12954
|
if (routing.locus === channelId)
|
|
12847
12955
|
return undefined;
|
|
12848
12956
|
const prevPin = routing.locus ?? null;
|
|
12849
|
-
routing
|
|
12957
|
+
setTurnLocus(routing, channelId, serverId);
|
|
12850
12958
|
const label = this.channelRegistry?.getDescriptor(channelId)?.label;
|
|
12851
12959
|
const shown = label && label !== channelId
|
|
12852
12960
|
? `${label.startsWith('#') ? label : `#${label}`} (${channelId})`
|
|
@@ -13102,11 +13210,13 @@ export class AgentFramework {
|
|
|
13102
13210
|
* draft's attempt is journaled durably before the request leaves, and its
|
|
13103
13211
|
* outcome after, exactly as a drafts resend would.
|
|
13104
13212
|
*/
|
|
13105
|
-
async deliverWithUnsent(agent, body, channelId, unsent
|
|
13213
|
+
async deliverWithUnsent(agent, body, channelId, unsent,
|
|
13214
|
+
/** The server the channel resolves with (triggerServerFor). */
|
|
13215
|
+
serverId) {
|
|
13106
13216
|
const registry = this.channelRegistry;
|
|
13107
13217
|
let attemptId;
|
|
13108
13218
|
if (unsent) {
|
|
13109
|
-
const target = registry.resolveDestination({ channelId });
|
|
13219
|
+
const target = registry.resolveDestination({ ...(serverId !== undefined ? { serverId } : {}), channelId });
|
|
13110
13220
|
if (!('error' in target)) {
|
|
13111
13221
|
try {
|
|
13112
13222
|
attemptId = this.proseDrafts.beginAttempt(agent.name, unsent.id, target.destination, 'unsent-token', false);
|
|
@@ -13128,7 +13238,7 @@ export class AgentFramework {
|
|
|
13128
13238
|
try {
|
|
13129
13239
|
// Both callers hold a failed delivery's words as a bounce draft, whose
|
|
13130
13240
|
// notice says what happened, so the registry writes no second marker.
|
|
13131
|
-
outcome = await registry.deliverSpeech(agent.name, body, channelId, { heldOnFailure: true });
|
|
13241
|
+
outcome = await registry.deliverSpeech(agent.name, body, channelId, { heldOnFailure: true, ...(serverId !== undefined ? { locusServerId: serverId } : {}) });
|
|
13132
13242
|
}
|
|
13133
13243
|
finally {
|
|
13134
13244
|
if (flight) {
|
|
@@ -13220,7 +13330,7 @@ export class AgentFramework {
|
|
|
13220
13330
|
continue;
|
|
13221
13331
|
}
|
|
13222
13332
|
try {
|
|
13223
|
-
const outcome = await this.deliverWithUnsent(agent, body, resolved.channelId, unsent.draft);
|
|
13333
|
+
const outcome = await this.deliverWithUnsent(agent, body, resolved.channelId, unsent.draft, triggerServerFor(routing, resolved.channelId));
|
|
13224
13334
|
this.recordProseDelivery(routing, outcome.status === 'delivered' ? { delivered: true, channelId: outcome.destination.channelId } : null);
|
|
13225
13335
|
if (outcome.status === 'delivered') {
|
|
13226
13336
|
this.proseBounceStreaks.delete(agent.name);
|
|
@@ -13264,7 +13374,7 @@ export class AgentFramework {
|
|
|
13264
13374
|
}
|
|
13265
13375
|
const target = sticky ?? locus;
|
|
13266
13376
|
try {
|
|
13267
|
-
const outcome = await this.channelRegistry.routeSpeech(agent.name, envelope, target);
|
|
13377
|
+
const outcome = await this.channelRegistry.routeSpeech(agent.name, envelope, target, speechServerOpts(routing, target));
|
|
13268
13378
|
this.recordProseDelivery(routing, outcome);
|
|
13269
13379
|
}
|
|
13270
13380
|
catch (err) {
|
|
@@ -13386,7 +13496,7 @@ export class AgentFramework {
|
|
|
13386
13496
|
return;
|
|
13387
13497
|
}
|
|
13388
13498
|
try {
|
|
13389
|
-
const outcome = await this.deliverWithUnsent(agent, body, targetChannel, unsent.draft);
|
|
13499
|
+
const outcome = await this.deliverWithUnsent(agent, body, targetChannel, unsent.draft, triggerServerFor(routing, targetChannel));
|
|
13390
13500
|
if (outcome.status === 'delivered') {
|
|
13391
13501
|
this.proseBounceStreaks.delete(name);
|
|
13392
13502
|
}
|
|
@@ -14073,6 +14183,9 @@ export class AgentFramework {
|
|
|
14073
14183
|
// turn, which falls back to the global default rather than inheriting
|
|
14074
14184
|
// a previous turn's channel.
|
|
14075
14185
|
routing.triggerChannel = trigger?.channelId;
|
|
14186
|
+
routing.triggerOrigin = trigger?.channelId && trigger.serverId
|
|
14187
|
+
? { channelId: trigger.channelId, serverId: trigger.serverId }
|
|
14188
|
+
: undefined;
|
|
14076
14189
|
// FREEZE the turn's outbound locus now (turn-frozen routing): the home
|
|
14077
14190
|
// channel for forks, else the trigger channel, else the global default,
|
|
14078
14191
|
// which an ephemeral run never takes (takesGlobalFallback).
|
|
@@ -14088,8 +14201,14 @@ export class AgentFramework {
|
|
|
14088
14201
|
// stability).
|
|
14089
14202
|
if (turnProseRouting !== 'explicit' && turnProseRouting !== 'disabled') {
|
|
14090
14203
|
const locus = this.channelRegistry?.resolveLocus(agent.name) ?? null;
|
|
14091
|
-
|
|
14092
|
-
|
|
14204
|
+
// A locus that is neither the trigger's channel nor the agent's home
|
|
14205
|
+
// is the global fallback: its server is the latest accepted
|
|
14206
|
+
// message's, taken now, with the locus.
|
|
14207
|
+
if (locus !== null) {
|
|
14208
|
+
setTurnLocus(routing, locus, locus !== routing.triggerChannel && !this.conversationAgentHomes.has(agent.name)
|
|
14209
|
+
? this.channelRegistry?.fallbackServerFor(locus)
|
|
14210
|
+
: undefined);
|
|
14211
|
+
}
|
|
14093
14212
|
if (attempt === 0)
|
|
14094
14213
|
this.announceLocusIfChanged(agent.name, locus);
|
|
14095
14214
|
}
|
|
@@ -14105,7 +14224,7 @@ export class AgentFramework {
|
|
|
14105
14224
|
: undefined;
|
|
14106
14225
|
routing.arrivals = [];
|
|
14107
14226
|
if (arrival)
|
|
14108
|
-
this.repinTurnLocus(agent, routing, arrival, 'arrival');
|
|
14227
|
+
this.repinTurnLocus(agent, routing, arrival.channelId, 'arrival', arrival.serverId);
|
|
14109
14228
|
}
|
|
14110
14229
|
// A silent start, or a disabled agent's, has no locus to show, whatever a
|
|
14111
14230
|
// channel_open in a silent turn left on its object.
|
|
@@ -14624,7 +14743,7 @@ export class AgentFramework {
|
|
|
14624
14743
|
};
|
|
14625
14744
|
const enqueueSpeech = (text, locus) => {
|
|
14626
14745
|
chainSpeech(async () => {
|
|
14627
|
-
const outcome = await this.channelRegistry.routeSpeech(agent.name, text, locus);
|
|
14746
|
+
const outcome = await this.channelRegistry.routeSpeech(agent.name, text, locus, speechServerOpts(streamRouting, locus));
|
|
14628
14747
|
this.recordProseDelivery(streamRouting, outcome);
|
|
14629
14748
|
}, 'mid-turn speech routing failed:');
|
|
14630
14749
|
};
|
|
@@ -14739,7 +14858,7 @@ export class AgentFramework {
|
|
|
14739
14858
|
: null;
|
|
14740
14859
|
const emitOutgoing = (deltas) => {
|
|
14741
14860
|
for (const rd of deltas) {
|
|
14742
|
-
this.channelRegistry.sendOutgoingChunk(rd.channelId, agent.name, outgoingInferenceId, outgoingIndex++, rd.delta);
|
|
14861
|
+
this.channelRegistry.sendOutgoingChunk(rd.channelId, agent.name, outgoingInferenceId, outgoingIndex++, rd.delta, triggerServerFor(streamRouting, rd.channelId));
|
|
14743
14862
|
}
|
|
14744
14863
|
};
|
|
14745
14864
|
// Tool-result-guard publication boundary. While a SUBMITTED batch is
|
|
@@ -15587,7 +15706,7 @@ export class AgentFramework {
|
|
|
15587
15706
|
console.error(`[routing] ${agent.name}: text-only turn -> routing speech -> ${locus ?? '(none)'}`);
|
|
15588
15707
|
for (const segment of speechSegments) {
|
|
15589
15708
|
try {
|
|
15590
|
-
const outcome = await this.channelRegistry.routeSpeech(agent.name, segment, locus);
|
|
15709
|
+
const outcome = await this.channelRegistry.routeSpeech(agent.name, segment, locus, speechServerOpts(streamRouting, locus));
|
|
15591
15710
|
this.recordProseDelivery(streamRouting, outcome);
|
|
15592
15711
|
}
|
|
15593
15712
|
catch (err) {
|
|
@@ -15681,7 +15800,7 @@ export class AgentFramework {
|
|
|
15681
15800
|
// Deliver sequentially (await each) so the segments land in order.
|
|
15682
15801
|
for (const seg of segments) {
|
|
15683
15802
|
try {
|
|
15684
|
-
const outcome = await this.channelRegistry.routeSpeech(agent.name, seg, locus);
|
|
15803
|
+
const outcome = await this.channelRegistry.routeSpeech(agent.name, seg, locus, speechServerOpts(streamRouting, locus));
|
|
15685
15804
|
this.recordProseDelivery(streamRouting, outcome);
|
|
15686
15805
|
}
|
|
15687
15806
|
catch (err) {
|
|
@@ -16327,7 +16446,7 @@ export class AgentFramework {
|
|
|
16327
16446
|
if (frameReachedTerminal && proseStream) {
|
|
16328
16447
|
emitOutgoing(proseStream.finish());
|
|
16329
16448
|
for (const [channelId, text] of proseStream.byChannel()) {
|
|
16330
|
-
this.channelRegistry.sendOutgoingComplete(channelId, agent.name, outgoingInferenceId, text);
|
|
16449
|
+
this.channelRegistry.sendOutgoingComplete(channelId, agent.name, outgoingInferenceId, text, triggerServerFor(streamRouting, channelId));
|
|
16331
16450
|
}
|
|
16332
16451
|
}
|
|
16333
16452
|
this.frameworkCancelledStreams.delete(`${agent.name}:${myStreamId}`);
|
|
@@ -16602,7 +16721,7 @@ export class AgentFramework {
|
|
|
16602
16721
|
// agent origin and acts as that resident, as the model's own call does
|
|
16603
16722
|
// through dispatchToolCall. A module holds no drafts.
|
|
16604
16723
|
if (call.name === 'drafts' && this.channelRegistry && origin.kind === 'agent') {
|
|
16605
|
-
return this.draftsToolResult(origin.agentName, call.input);
|
|
16724
|
+
return this.draftsToolResult(origin.agentName, call.input, call.callerTurnId);
|
|
16606
16725
|
}
|
|
16607
16726
|
// The utils meta-tool — programmatic callers (code_execution scripts,
|
|
16608
16727
|
// ephemerals, ModuleContext.callTool) reach utilities the same way the
|
|
@@ -18079,7 +18198,7 @@ export class AgentFramework {
|
|
|
18079
18198
|
if (enrichedCall.name === 'drafts' && this.channelRegistry) {
|
|
18080
18199
|
const startedAt = Date.now();
|
|
18081
18200
|
this.emitTrace({ type: 'tool:started', module: 'framework', tool: 'drafts', callId: enrichedCall.id, input: enrichedCall.input });
|
|
18082
|
-
void this.draftsToolResult(agentName, enrichedCall.input)
|
|
18201
|
+
void this.draftsToolResult(agentName, enrichedCall.input, enrichedCall.callerTurnId)
|
|
18083
18202
|
.then((result) => {
|
|
18084
18203
|
this.emitTrace({ type: 'tool:completed', module: 'framework', tool: 'drafts', callId: enrichedCall.id, durationMs: Date.now() - startedAt });
|
|
18085
18204
|
this.pushEvent({ type: 'tool-result', callId: enrichedCall.id, toolName: enrichedCall.name, agentName, moduleName: 'framework', result });
|
|
@@ -19471,7 +19590,7 @@ export class AgentFramework {
|
|
|
19471
19590
|
const source = this.pushSource(event);
|
|
19472
19591
|
this.noteInboundAccepted(source);
|
|
19473
19592
|
return source;
|
|
19474
|
-
});
|
|
19593
|
+
}, (serverId, params) => this.channelOfAnotherServer(serverId, params.origin));
|
|
19475
19594
|
// Server-initiated inference router (Step 6)
|
|
19476
19595
|
this.inferenceRouter = new InferenceRouter(this.membrane, this.hookOrchestrator, this.featureSetManager, inferenceRouting ?? null, (event) => this.emitTrace(event));
|
|
19477
19596
|
// Checkpoint manager (Step 8), built and loaded at the top
|
|
@@ -22736,7 +22855,16 @@ export class AgentFramework {
|
|
|
22736
22855
|
// ordinary wake, starts only once its over-budget boundary has every
|
|
22737
22856
|
// result of the round.
|
|
22738
22857
|
const callingTurn = this.routingOf(agentName);
|
|
22739
|
-
|
|
22858
|
+
const inTurn = call.callerTurnId !== undefined && call.callerTurnId === callingTurn.turnId;
|
|
22859
|
+
this.channelRegistry.handleChannelToolCall(call.name, call.input, {
|
|
22860
|
+
kind: 'agent',
|
|
22861
|
+
agentName,
|
|
22862
|
+
// The turn's recorded servers go only with a call made inside that
|
|
22863
|
+
// turn: a background script's call carries no turn, and takes a bare
|
|
22864
|
+
// id only where exactly one server registers it.
|
|
22865
|
+
...(inTurn && callingTurn.triggerOrigin !== undefined ? { trigger: callingTurn.triggerOrigin } : {}),
|
|
22866
|
+
...(inTurn && callingTurn.locusOrigin !== undefined ? { locus: callingTurn.locusOrigin } : {}),
|
|
22867
|
+
})
|
|
22740
22868
|
.then((result) => {
|
|
22741
22869
|
const durationMs = Date.now() - startTime;
|
|
22742
22870
|
this.emitTrace({
|
|
@@ -22773,7 +22901,13 @@ export class AgentFramework {
|
|
|
22773
22901
|
if (openerAgent
|
|
22774
22902
|
&& (openerAgent.proseRouting === 'locus' || openerAgent.proseRouting === 'hybrid')
|
|
22775
22903
|
&& this.openLogical.get(agentName) === callingTurn) {
|
|
22776
|
-
|
|
22904
|
+
const openedOn = this.channelRegistry.resolveDestination({
|
|
22905
|
+
...(typeof call.input?.serverId === 'string'
|
|
22906
|
+
? { serverId: call.input.serverId }
|
|
22907
|
+
: {}),
|
|
22908
|
+
channelId: opened,
|
|
22909
|
+
});
|
|
22910
|
+
setTurnLocus(callingTurn, opened, 'error' in openedOn ? undefined : openedOn.destination.serverId);
|
|
22777
22911
|
this.lastAnnouncedLocus.set(agentName, opened);
|
|
22778
22912
|
result = {
|
|
22779
22913
|
...result,
|
|
@@ -23026,9 +23160,10 @@ export class AgentFramework {
|
|
|
23026
23160
|
else if (agent?.proseRouting === 'explicit') {
|
|
23027
23161
|
// Explicit mode: announce to the turn's sticky prose target if the
|
|
23028
23162
|
// model has set one; otherwise stay quiet — never guess a channel.
|
|
23029
|
-
const
|
|
23163
|
+
const routing = this.turnRouting.get(agentName);
|
|
23164
|
+
const target = routing?.pin;
|
|
23030
23165
|
if (target) {
|
|
23031
|
-
this.channelRegistry.routeSpeech(agentName, text, target).catch((err) => {
|
|
23166
|
+
this.channelRegistry.routeSpeech(agentName, text, target, speechServerOpts(call.callerTurnId === routing?.turnId ? routing : undefined, target)).catch((err) => {
|
|
23032
23167
|
console.error('[sleep] announce failed:', err instanceof Error ? err.message : err);
|
|
23033
23168
|
});
|
|
23034
23169
|
}
|
|
@@ -23037,8 +23172,9 @@ export class AgentFramework {
|
|
|
23037
23172
|
}
|
|
23038
23173
|
}
|
|
23039
23174
|
else {
|
|
23040
|
-
const
|
|
23041
|
-
|
|
23175
|
+
const routing = this.turnRouting.get(agentName);
|
|
23176
|
+
const locus = routing?.locus ?? null;
|
|
23177
|
+
this.channelRegistry.routeSpeech(agentName, text, locus, speechServerOpts(call.callerTurnId === routing?.turnId ? routing : undefined, locus)).catch((err) => {
|
|
23042
23178
|
console.error('[sleep] announce failed:', err instanceof Error ? err.message : err);
|
|
23043
23179
|
});
|
|
23044
23180
|
}
|