@animalabs/agent-framework 0.22.5 → 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 +15 -1
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +194 -56
- 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 +222 -48
- 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
|
@@ -363,6 +363,8 @@ export class ChannelRegistry {
|
|
|
363
363
|
dmClassified = new Map();
|
|
364
364
|
/** Most recent incoming channel ID — used for speech routing / default publish. */
|
|
365
365
|
defaultPublishChannel = null;
|
|
366
|
+
/** The server whose accepted message set defaultPublishChannel. */
|
|
367
|
+
defaultPublishServer = null;
|
|
366
368
|
/** Most recent incoming message metadata, used for buildChannelContext. */
|
|
367
369
|
defaultPublishMessageId = null;
|
|
368
370
|
defaultPublishThreadId = undefined;
|
|
@@ -445,7 +447,7 @@ export class ChannelRegistry {
|
|
|
445
447
|
descriptor: channel,
|
|
446
448
|
open: false,
|
|
447
449
|
});
|
|
448
|
-
this.appendLabelSighting(channel.id, channel.label, extractDmMeta(channel.metadata));
|
|
450
|
+
this.appendLabelSighting(serverId, channel.id, channel.label, extractDmMeta(channel.metadata));
|
|
449
451
|
registeredIds.push(channel.id);
|
|
450
452
|
results.push({ id: channel.id, accepted: true });
|
|
451
453
|
}
|
|
@@ -506,7 +508,7 @@ export class ChannelRegistry {
|
|
|
506
508
|
continue;
|
|
507
509
|
}
|
|
508
510
|
existing.descriptor = channel;
|
|
509
|
-
this.appendLabelSighting(channel.id, channel.label, extractDmMeta(channel.metadata));
|
|
511
|
+
this.appendLabelSighting(serverId, channel.id, channel.label, extractDmMeta(channel.metadata));
|
|
510
512
|
addedResults.push({ id: channel.id, accepted: true });
|
|
511
513
|
}
|
|
512
514
|
}
|
|
@@ -526,7 +528,7 @@ export class ChannelRegistry {
|
|
|
526
528
|
descriptor: channel,
|
|
527
529
|
open: false,
|
|
528
530
|
});
|
|
529
|
-
this.appendLabelSighting(channel.id, channel.label, extractDmMeta(channel.metadata));
|
|
531
|
+
this.appendLabelSighting(serverId, channel.id, channel.label, extractDmMeta(channel.metadata));
|
|
530
532
|
addedResults.push({ id: channel.id, accepted: true });
|
|
531
533
|
accepted.push(channel);
|
|
532
534
|
}
|
|
@@ -708,6 +710,7 @@ export class ChannelRegistry {
|
|
|
708
710
|
// for those this runs after the hook (below).
|
|
709
711
|
const markAccepted = () => {
|
|
710
712
|
this.defaultPublishChannel = message.channelId;
|
|
713
|
+
this.defaultPublishServer = serverId;
|
|
711
714
|
this.defaultPublishMessageId = message.messageId;
|
|
712
715
|
this.defaultPublishThreadId = message.threadId;
|
|
713
716
|
// A server sending channels/incoming is authoritative evidence that
|
|
@@ -819,7 +822,9 @@ export class ChannelRegistry {
|
|
|
819
822
|
* richer one.
|
|
820
823
|
*/
|
|
821
824
|
ensureChannelRegistered(serverId, channelId, label, extraMetadata) {
|
|
822
|
-
|
|
825
|
+
// The pushing server's own entry: another server's entry for the same id
|
|
826
|
+
// is that server's, and a push leaves it as it was.
|
|
827
|
+
const existing = this.channels.get(`${serverId}:${channelId}`);
|
|
823
828
|
if (existing) {
|
|
824
829
|
// Backfill identity onto a bare lazy registration: a DM that first
|
|
825
830
|
// arrived with no label/recipient info gets a usable name the next
|
|
@@ -833,7 +838,7 @@ export class ChannelRegistry {
|
|
|
833
838
|
// Recorded before the live descriptor takes it, since this branch's
|
|
834
839
|
// guard reads the descriptor: an append that throws leaves the
|
|
835
840
|
// placeholder, so the next push tries again.
|
|
836
|
-
this.appendLabelSighting(existing.descriptor.id, label, extractDmMeta(metadata));
|
|
841
|
+
this.appendLabelSighting(serverId, existing.descriptor.id, label, extractDmMeta(metadata));
|
|
837
842
|
existing.descriptor.label = label;
|
|
838
843
|
existing.descriptor.metadata = metadata;
|
|
839
844
|
}
|
|
@@ -853,7 +858,7 @@ export class ChannelRegistry {
|
|
|
853
858
|
// label to record. A real label is recorded before the entry exists:
|
|
854
859
|
// an append that throws registers nothing, so the next push tries again.
|
|
855
860
|
if (label)
|
|
856
|
-
this.appendLabelSighting(channelId, label, extractDmMeta(extraMetadata));
|
|
861
|
+
this.appendLabelSighting(serverId, channelId, label, extractDmMeta(extraMetadata));
|
|
857
862
|
const key = `${serverId}:${channelId}`;
|
|
858
863
|
this.channels.set(key, {
|
|
859
864
|
serverId,
|
|
@@ -1044,7 +1049,7 @@ export class ChannelRegistry {
|
|
|
1044
1049
|
case 'channel_decline':
|
|
1045
1050
|
return this.handleToolDecline(input);
|
|
1046
1051
|
case 'channel_publish':
|
|
1047
|
-
return this.handleToolPublish(input);
|
|
1052
|
+
return this.handleToolPublish(input, origin);
|
|
1048
1053
|
case 'think':
|
|
1049
1054
|
return this.handleToolThink(input);
|
|
1050
1055
|
case 'journal':
|
|
@@ -1115,8 +1120,11 @@ export class ChannelRegistry {
|
|
|
1115
1120
|
continue;
|
|
1116
1121
|
this.channels.delete(key);
|
|
1117
1122
|
this.stopTyping(entry.descriptor.id);
|
|
1118
|
-
|
|
1123
|
+
// The fallback goes with the server whose message set it, not with
|
|
1124
|
+
// another server's entry for the same id.
|
|
1125
|
+
if (this.defaultPublishChannel === entry.descriptor.id && this.defaultPublishServer === serverId) {
|
|
1119
1126
|
this.defaultPublishChannel = null;
|
|
1127
|
+
this.defaultPublishServer = null;
|
|
1120
1128
|
this.defaultPublishMessageId = null;
|
|
1121
1129
|
this.defaultPublishThreadId = undefined;
|
|
1122
1130
|
}
|
|
@@ -1136,6 +1144,7 @@ export class ChannelRegistry {
|
|
|
1136
1144
|
this.channels.clear();
|
|
1137
1145
|
// Reset default publish tracking
|
|
1138
1146
|
this.defaultPublishChannel = null;
|
|
1147
|
+
this.defaultPublishServer = null;
|
|
1139
1148
|
this.defaultPublishMessageId = null;
|
|
1140
1149
|
this.defaultPublishThreadId = undefined;
|
|
1141
1150
|
}
|
|
@@ -1269,9 +1278,15 @@ export class ChannelRegistry {
|
|
|
1269
1278
|
* `HistoryModule` tool call passes through unguarded via
|
|
1270
1279
|
* `resolveProseTargetDurable()`.
|
|
1271
1280
|
*/
|
|
1272
|
-
appendLabelSighting(channelId, label, dmMeta = {}) {
|
|
1281
|
+
appendLabelSighting(serverId, channelId, label, dmMeta = {}) {
|
|
1273
1282
|
if (typeof label !== 'string' || !label)
|
|
1274
1283
|
return;
|
|
1284
|
+
// The log is kept by the bare id, and a durable lookup by label resolves
|
|
1285
|
+
// to that id with no server. So a server's label for an id another
|
|
1286
|
+
// server registers isn't recorded: it would name the other server's
|
|
1287
|
+
// channel, across restarts.
|
|
1288
|
+
if (this.registeredByAnotherServer(serverId, channelId))
|
|
1289
|
+
return;
|
|
1275
1290
|
const { recipientId, recipientName, isDm } = dmMeta;
|
|
1276
1291
|
// Never let a bare-id placeholder (ensureChannelRegistered's fallback
|
|
1277
1292
|
// `label ?? channelId` when no real label was ever supplied — e.g. a
|
|
@@ -1842,10 +1857,26 @@ export class ChannelRegistry {
|
|
|
1842
1857
|
.filter((e) => e.serverId === serverId)
|
|
1843
1858
|
.map((e) => e.descriptor);
|
|
1844
1859
|
}
|
|
1860
|
+
/**
|
|
1861
|
+
* The server whose accepted message set the global fallback, where
|
|
1862
|
+
* `channelId` is the fallback's channel: a turn whose locus is the
|
|
1863
|
+
* fallback resolves it with this server, taken with the locus.
|
|
1864
|
+
*/
|
|
1865
|
+
fallbackServerFor(channelId) {
|
|
1866
|
+
return this.defaultPublishChannel === channelId ? this.defaultPublishServer ?? undefined : undefined;
|
|
1867
|
+
}
|
|
1845
1868
|
/** Server id owning a registered channel (by descriptor id), or null. */
|
|
1846
1869
|
getChannelServerId(channelId) {
|
|
1847
1870
|
return this.findChannelEntry(channelId)?.serverId ?? null;
|
|
1848
1871
|
}
|
|
1872
|
+
/** Whether a server other than `serverId` registers `channelId`. */
|
|
1873
|
+
registeredByAnotherServer(serverId, channelId) {
|
|
1874
|
+
for (const entry of this.channels.values()) {
|
|
1875
|
+
if (entry.descriptor.id === channelId && entry.serverId !== serverId)
|
|
1876
|
+
return true;
|
|
1877
|
+
}
|
|
1878
|
+
return false;
|
|
1879
|
+
}
|
|
1849
1880
|
/**
|
|
1850
1881
|
* Find a channel entry by channelId (searches across all servers).
|
|
1851
1882
|
* Returns the first match.
|
|
@@ -2451,8 +2482,11 @@ export class ChannelRegistry {
|
|
|
2451
2482
|
* Fire-and-forget and never throws: streaming is an observer surface; the
|
|
2452
2483
|
* authoritative delivery is the eventual channels/publish.
|
|
2453
2484
|
*/
|
|
2454
|
-
sendOutgoingChunk(channelId, conversationId, inferenceId, index, delta
|
|
2455
|
-
|
|
2485
|
+
sendOutgoingChunk(channelId, conversationId, inferenceId, index, delta,
|
|
2486
|
+
/** The server the channel resolves with, as the speech's locus does
|
|
2487
|
+
* (deliverSpeech's locusServerId). */
|
|
2488
|
+
serverId) {
|
|
2489
|
+
const server = this.streamingServerFor(channelId, serverId);
|
|
2456
2490
|
if (!server)
|
|
2457
2491
|
return;
|
|
2458
2492
|
try {
|
|
@@ -2461,8 +2495,10 @@ export class ChannelRegistry {
|
|
|
2461
2495
|
catch { /* observer surface — never disturb the turn */ }
|
|
2462
2496
|
}
|
|
2463
2497
|
/** Spec 14.3 companion: final moderated content per channel at stream end. */
|
|
2464
|
-
sendOutgoingComplete(channelId, conversationId, inferenceId, text
|
|
2465
|
-
|
|
2498
|
+
sendOutgoingComplete(channelId, conversationId, inferenceId, text,
|
|
2499
|
+
/** As sendOutgoingChunk's. */
|
|
2500
|
+
serverId) {
|
|
2501
|
+
const server = this.streamingServerFor(channelId, serverId);
|
|
2466
2502
|
if (!server)
|
|
2467
2503
|
return;
|
|
2468
2504
|
try {
|
|
@@ -2475,15 +2511,19 @@ export class ChannelRegistry {
|
|
|
2475
2511
|
}
|
|
2476
2512
|
catch { /* observer surface — never disturb the turn */ }
|
|
2477
2513
|
}
|
|
2478
|
-
streamingServerFor(channelId) {
|
|
2514
|
+
streamingServerFor(channelId, serverId) {
|
|
2479
2515
|
// The map is keyed `${serverId}:${channelId}`; stream targets arrive as
|
|
2480
|
-
// bare descriptor ids (what resolveProseTarget returns).
|
|
2481
|
-
//
|
|
2482
|
-
//
|
|
2516
|
+
// bare descriptor ids (what resolveProseTarget returns). With the server
|
|
2517
|
+
// the speech resolves with, only that server's entry; otherwise scan like
|
|
2518
|
+
// the resolver does, and fail closed on cross-server ambiguity — the
|
|
2519
|
+
// same never-guess rule that governs delivery.
|
|
2483
2520
|
const matches = [...this.channels.values()].filter((e) => e.descriptor.id === channelId);
|
|
2484
|
-
|
|
2521
|
+
const entry = serverId !== undefined
|
|
2522
|
+
? matches.find((e) => e.serverId === serverId)
|
|
2523
|
+
: matches.length === 1 ? matches[0] : undefined;
|
|
2524
|
+
if (!entry)
|
|
2485
2525
|
return null;
|
|
2486
|
-
const server = this.serverRegistry.getServer(
|
|
2526
|
+
const server = this.serverRegistry.getServer(entry.serverId);
|
|
2487
2527
|
// §5.4: the GRANT gates streaming, not the raw advertisement. The old
|
|
2488
2528
|
// `capabilities?.channels?.streaming` check was doubly wrong: undefined
|
|
2489
2529
|
// for the boolean `channels: true` shape (masking discord-mcpl's latent
|
|
@@ -2659,7 +2699,12 @@ export class ChannelRegistry {
|
|
|
2659
2699
|
locusChannelId,
|
|
2660
2700
|
/** `heldOnFailure`: the caller holds a `failed` delivery's words as a
|
|
2661
2701
|
* draft with its own notice, so no failure marker is written for it.
|
|
2662
|
-
* An `unknown` outcome still gets its marker.
|
|
2702
|
+
* An `unknown` outcome still gets its marker. `locusServerId`: the
|
|
2703
|
+
* server the locus resolves with, snapshotted by the caller with the
|
|
2704
|
+
* locus: the server whose message triggered the turn, where the locus is
|
|
2705
|
+
* that message's channel. The speech goes out through that server only,
|
|
2706
|
+
* and fails where it no longer registers the channel. Without it, the
|
|
2707
|
+
* locus is taken only where exactly one server registers it. */
|
|
2663
2708
|
opts) {
|
|
2664
2709
|
const fail = (channelId, reason, outcome) => {
|
|
2665
2710
|
const status = outcome.status === 'unknown' ? 'unknown' : 'failed';
|
|
@@ -2694,7 +2739,11 @@ export class ChannelRegistry {
|
|
|
2694
2739
|
const reason = 'turn has no locus (no home/trigger channel; nothing to deliver into)';
|
|
2695
2740
|
return fail(null, reason, { status: 'failed', reason, at: Date.now() });
|
|
2696
2741
|
}
|
|
2697
|
-
const outcome = await this.publish(conversationId, text, {
|
|
2742
|
+
const outcome = await this.publish(conversationId, text, {
|
|
2743
|
+
...(opts?.locusServerId !== undefined ? { serverId: opts.locusServerId } : {}),
|
|
2744
|
+
channelId: locusChannelId,
|
|
2745
|
+
routed: true,
|
|
2746
|
+
});
|
|
2698
2747
|
const channelId = outcome.destination?.channelId ?? locusChannelId;
|
|
2699
2748
|
if (outcome.status !== 'delivered') {
|
|
2700
2749
|
return fail(channelId, outcome.reason ?? 'delivery not confirmed', outcome);
|
|
@@ -2715,9 +2764,10 @@ export class ChannelRegistry {
|
|
|
2715
2764
|
});
|
|
2716
2765
|
return outcome;
|
|
2717
2766
|
}
|
|
2718
|
-
/** deliverSpeech, reduced to the confirmed delivery (null when there was
|
|
2719
|
-
|
|
2720
|
-
|
|
2767
|
+
/** deliverSpeech, reduced to the confirmed delivery (null when there was
|
|
2768
|
+
* none). `opts.locusServerId` is deliverSpeech's. */
|
|
2769
|
+
async routeSpeech(conversationId, text, locusChannelId, opts) {
|
|
2770
|
+
const outcome = await this.deliverSpeech(conversationId, text, locusChannelId, opts?.locusServerId !== undefined ? { locusServerId: opts.locusServerId } : undefined);
|
|
2721
2771
|
if (outcome.status !== 'delivered')
|
|
2722
2772
|
return null;
|
|
2723
2773
|
return {
|
|
@@ -2726,7 +2776,7 @@ export class ChannelRegistry {
|
|
|
2726
2776
|
...(outcome.messageId !== undefined ? { messageId: outcome.messageId } : {}),
|
|
2727
2777
|
};
|
|
2728
2778
|
}
|
|
2729
|
-
async handleToolPublish(input) {
|
|
2779
|
+
async handleToolPublish(input, origin) {
|
|
2730
2780
|
// Resolve content: accept both `content` and `text` (backward compat)
|
|
2731
2781
|
const messageText = input.content ?? input.text;
|
|
2732
2782
|
if (!messageText) {
|
|
@@ -2745,11 +2795,30 @@ export class ChannelRegistry {
|
|
|
2745
2795
|
isError: true,
|
|
2746
2796
|
};
|
|
2747
2797
|
}
|
|
2748
|
-
|
|
2798
|
+
// A serverId the call gives names the exact pair, as the other channel
|
|
2799
|
+
// tools read theirs (this tool's schema doesn't declare one); with no
|
|
2800
|
+
// channelId, it's the default channel on that server. A call naming no
|
|
2801
|
+
// server takes the calling turn's server for its trigger's or locus's
|
|
2802
|
+
// channel; else, for the default channel, the server whose accepted
|
|
2803
|
+
// message set it; else a channelId only where exactly one server
|
|
2804
|
+
// registers it.
|
|
2805
|
+
const serverId = input.serverId;
|
|
2806
|
+
const trigger = !serverId && origin?.kind === 'agent'
|
|
2807
|
+
? [origin.trigger, origin.locus].find((o) => o?.channelId === channelId)
|
|
2808
|
+
: undefined;
|
|
2809
|
+
const defaultServer = !serverId && trigger === undefined && input.channelId === undefined
|
|
2810
|
+
? this.fallbackServerFor(channelId)
|
|
2811
|
+
: undefined;
|
|
2812
|
+
const resolved = this.resolveToolChannelEntry(channelId, serverId || (trigger?.serverId ?? defaultServer));
|
|
2813
|
+
const entry = resolved.entry;
|
|
2749
2814
|
if (!entry) {
|
|
2750
2815
|
return {
|
|
2751
2816
|
success: false,
|
|
2752
|
-
error:
|
|
2817
|
+
error: trigger !== undefined
|
|
2818
|
+
? `Channel ${channelId} is no longer registered by the server whose message brought you there, so nothing was published.`
|
|
2819
|
+
: defaultServer !== undefined
|
|
2820
|
+
? `Channel ${channelId}, which this call took as its default, is no longer registered by the server whose message set that default, so nothing was published.`
|
|
2821
|
+
: resolved.error,
|
|
2753
2822
|
isError: true,
|
|
2754
2823
|
};
|
|
2755
2824
|
}
|