@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
|
@@ -420,6 +420,8 @@ export class ChannelRegistry {
|
|
|
420
420
|
dmClassified = new Map();
|
|
421
421
|
/** Most recent incoming channel ID — used for speech routing / default publish. */
|
|
422
422
|
defaultPublishChannel = null;
|
|
423
|
+
/** The server whose accepted message set defaultPublishChannel. */
|
|
424
|
+
defaultPublishServer = null;
|
|
423
425
|
/** Most recent incoming message metadata, used for buildChannelContext. */
|
|
424
426
|
defaultPublishMessageId = null;
|
|
425
427
|
defaultPublishThreadId = undefined;
|
|
@@ -534,7 +536,7 @@ export class ChannelRegistry {
|
|
|
534
536
|
}
|
|
535
537
|
const key = `${serverId}:${channel.id}`;
|
|
536
538
|
this.registerEntry(key, serverId, channel);
|
|
537
|
-
this.appendLabelSighting(channel.id, channel.label, extractDmMeta(channel.metadata));
|
|
539
|
+
this.appendLabelSighting(serverId, channel.id, channel.label, extractDmMeta(channel.metadata));
|
|
538
540
|
registeredIds.push(channel.id);
|
|
539
541
|
results.push({ id: channel.id, accepted: true });
|
|
540
542
|
}
|
|
@@ -611,7 +613,7 @@ export class ChannelRegistry {
|
|
|
611
613
|
}
|
|
612
614
|
}
|
|
613
615
|
existing.descriptor = channel;
|
|
614
|
-
this.appendLabelSighting(channel.id, channel.label, extractDmMeta(channel.metadata));
|
|
616
|
+
this.appendLabelSighting(serverId, channel.id, channel.label, extractDmMeta(channel.metadata));
|
|
615
617
|
addedResults.push({ id: channel.id, accepted: true });
|
|
616
618
|
}
|
|
617
619
|
}
|
|
@@ -627,7 +629,7 @@ export class ChannelRegistry {
|
|
|
627
629
|
}
|
|
628
630
|
const key = `${serverId}:${channel.id}`;
|
|
629
631
|
this.registerEntry(key, serverId, channel);
|
|
630
|
-
this.appendLabelSighting(channel.id, channel.label, extractDmMeta(channel.metadata));
|
|
632
|
+
this.appendLabelSighting(serverId, channel.id, channel.label, extractDmMeta(channel.metadata));
|
|
631
633
|
addedResults.push({ id: channel.id, accepted: true });
|
|
632
634
|
accepted.push(channel);
|
|
633
635
|
}
|
|
@@ -811,6 +813,7 @@ export class ChannelRegistry {
|
|
|
811
813
|
const incomingEntry = this.channels.get(incomingKey);
|
|
812
814
|
const markAccepted = () => {
|
|
813
815
|
this.defaultPublishChannel = message.channelId;
|
|
816
|
+
this.defaultPublishServer = serverId;
|
|
814
817
|
this.defaultPublishMessageId = message.messageId;
|
|
815
818
|
this.defaultPublishThreadId = message.threadId;
|
|
816
819
|
// Where the host directs this server's lifecycle, only a receipt for
|
|
@@ -940,7 +943,9 @@ export class ChannelRegistry {
|
|
|
940
943
|
* richer one.
|
|
941
944
|
*/
|
|
942
945
|
ensureChannelRegistered(serverId, channelId, label, extraMetadata) {
|
|
943
|
-
|
|
946
|
+
// The pushing server's own entry: another server's entry for the same id
|
|
947
|
+
// is that server's, and a push leaves it as it was.
|
|
948
|
+
const existing = this.channels.get(`${serverId}:${channelId}`);
|
|
944
949
|
if (existing) {
|
|
945
950
|
// Backfill identity onto a bare lazy registration: a DM that first
|
|
946
951
|
// arrived with no label/recipient info gets a usable name the next
|
|
@@ -954,7 +959,7 @@ export class ChannelRegistry {
|
|
|
954
959
|
// Recorded before the live descriptor takes it, since this branch's
|
|
955
960
|
// guard reads the descriptor: an append that throws leaves the
|
|
956
961
|
// placeholder, so the next push tries again.
|
|
957
|
-
this.appendLabelSighting(existing.descriptor.id, label, extractDmMeta(metadata));
|
|
962
|
+
this.appendLabelSighting(serverId, existing.descriptor.id, label, extractDmMeta(metadata));
|
|
958
963
|
existing.descriptor.label = label;
|
|
959
964
|
existing.descriptor.metadata = metadata;
|
|
960
965
|
}
|
|
@@ -974,7 +979,7 @@ export class ChannelRegistry {
|
|
|
974
979
|
// label to record. A real label is recorded before the entry exists:
|
|
975
980
|
// an append that throws registers nothing, so the next push tries again.
|
|
976
981
|
if (label)
|
|
977
|
-
this.appendLabelSighting(channelId, label, extractDmMeta(extraMetadata));
|
|
982
|
+
this.appendLabelSighting(serverId, channelId, label, extractDmMeta(extraMetadata));
|
|
978
983
|
const key = `${serverId}:${channelId}`;
|
|
979
984
|
this.channels.set(key, {
|
|
980
985
|
serverId,
|
|
@@ -1246,8 +1251,11 @@ export class ChannelRegistry {
|
|
|
1246
1251
|
continue;
|
|
1247
1252
|
this.channels.delete(key);
|
|
1248
1253
|
this.stopTyping(entry.descriptor.id);
|
|
1249
|
-
|
|
1254
|
+
// The fallback goes with the server whose message set it, not with
|
|
1255
|
+
// another server's entry for the same id.
|
|
1256
|
+
if (this.defaultPublishChannel === entry.descriptor.id && this.defaultPublishServer === serverId) {
|
|
1250
1257
|
this.defaultPublishChannel = null;
|
|
1258
|
+
this.defaultPublishServer = null;
|
|
1251
1259
|
this.defaultPublishMessageId = null;
|
|
1252
1260
|
this.defaultPublishThreadId = undefined;
|
|
1253
1261
|
}
|
|
@@ -1267,6 +1275,7 @@ export class ChannelRegistry {
|
|
|
1267
1275
|
this.channels.clear();
|
|
1268
1276
|
// Reset default publish tracking
|
|
1269
1277
|
this.defaultPublishChannel = null;
|
|
1278
|
+
this.defaultPublishServer = null;
|
|
1270
1279
|
this.defaultPublishMessageId = null;
|
|
1271
1280
|
this.defaultPublishThreadId = undefined;
|
|
1272
1281
|
}
|
|
@@ -1400,9 +1409,15 @@ export class ChannelRegistry {
|
|
|
1400
1409
|
* `HistoryModule` tool call passes through unguarded via
|
|
1401
1410
|
* `resolveProseTargetDurable()`.
|
|
1402
1411
|
*/
|
|
1403
|
-
appendLabelSighting(channelId, label, dmMeta = {}) {
|
|
1412
|
+
appendLabelSighting(serverId, channelId, label, dmMeta = {}) {
|
|
1404
1413
|
if (typeof label !== 'string' || !label)
|
|
1405
1414
|
return;
|
|
1415
|
+
// The log is kept by the bare id, and a durable lookup by label resolves
|
|
1416
|
+
// to that id with no server. So a server's label for an id another
|
|
1417
|
+
// server registers isn't recorded: it would name the other server's
|
|
1418
|
+
// channel, across restarts.
|
|
1419
|
+
if (this.registeredByAnotherServer(serverId, channelId))
|
|
1420
|
+
return;
|
|
1406
1421
|
const { recipientId, recipientName, isDm } = dmMeta;
|
|
1407
1422
|
// Never let a bare-id placeholder (ensureChannelRegistered's fallback
|
|
1408
1423
|
// `label ?? channelId` when no real label was ever supplied — e.g. a
|
|
@@ -2176,10 +2191,26 @@ export class ChannelRegistry {
|
|
|
2176
2191
|
.filter((e) => e.serverId === serverId)
|
|
2177
2192
|
.map((e) => e.descriptor);
|
|
2178
2193
|
}
|
|
2194
|
+
/**
|
|
2195
|
+
* The server whose accepted message set the global fallback, where
|
|
2196
|
+
* `channelId` is the fallback's channel: a turn whose locus is the
|
|
2197
|
+
* fallback resolves it with this server, taken with the locus.
|
|
2198
|
+
*/
|
|
2199
|
+
fallbackServerFor(channelId) {
|
|
2200
|
+
return this.defaultPublishChannel === channelId ? this.defaultPublishServer ?? undefined : undefined;
|
|
2201
|
+
}
|
|
2179
2202
|
/** Server id owning a registered channel (by descriptor id), or null. */
|
|
2180
2203
|
getChannelServerId(channelId) {
|
|
2181
2204
|
return this.findChannelEntry(channelId)?.serverId ?? null;
|
|
2182
2205
|
}
|
|
2206
|
+
/** Whether a server other than `serverId` registers `channelId`. */
|
|
2207
|
+
registeredByAnotherServer(serverId, channelId) {
|
|
2208
|
+
for (const entry of this.channels.values()) {
|
|
2209
|
+
if (entry.descriptor.id === channelId && entry.serverId !== serverId)
|
|
2210
|
+
return true;
|
|
2211
|
+
}
|
|
2212
|
+
return false;
|
|
2213
|
+
}
|
|
2183
2214
|
/**
|
|
2184
2215
|
* Find a channel entry by channelId (searches across all servers).
|
|
2185
2216
|
* Returns the first match.
|
|
@@ -2829,8 +2860,11 @@ export class ChannelRegistry {
|
|
|
2829
2860
|
* Fire-and-forget and never throws: streaming is an observer surface; the
|
|
2830
2861
|
* authoritative delivery is the eventual channels/publish.
|
|
2831
2862
|
*/
|
|
2832
|
-
sendOutgoingChunk(channelId, conversationId, inferenceId, index, delta
|
|
2833
|
-
|
|
2863
|
+
sendOutgoingChunk(channelId, conversationId, inferenceId, index, delta,
|
|
2864
|
+
/** The server the channel resolves with, as the speech's locus does
|
|
2865
|
+
* (deliverSpeech's locusServerId). */
|
|
2866
|
+
serverId) {
|
|
2867
|
+
const server = this.streamingServerFor(channelId, serverId);
|
|
2834
2868
|
if (!server)
|
|
2835
2869
|
return;
|
|
2836
2870
|
try {
|
|
@@ -2839,8 +2873,10 @@ export class ChannelRegistry {
|
|
|
2839
2873
|
catch { /* observer surface — never disturb the turn */ }
|
|
2840
2874
|
}
|
|
2841
2875
|
/** Spec 14.3 companion: final moderated content per channel at stream end. */
|
|
2842
|
-
sendOutgoingComplete(channelId, conversationId, inferenceId, text
|
|
2843
|
-
|
|
2876
|
+
sendOutgoingComplete(channelId, conversationId, inferenceId, text,
|
|
2877
|
+
/** As sendOutgoingChunk's. */
|
|
2878
|
+
serverId) {
|
|
2879
|
+
const server = this.streamingServerFor(channelId, serverId);
|
|
2844
2880
|
if (!server)
|
|
2845
2881
|
return;
|
|
2846
2882
|
try {
|
|
@@ -2853,15 +2889,19 @@ export class ChannelRegistry {
|
|
|
2853
2889
|
}
|
|
2854
2890
|
catch { /* observer surface — never disturb the turn */ }
|
|
2855
2891
|
}
|
|
2856
|
-
streamingServerFor(channelId) {
|
|
2892
|
+
streamingServerFor(channelId, serverId) {
|
|
2857
2893
|
// The map is keyed `${serverId}:${channelId}`; stream targets arrive as
|
|
2858
|
-
// bare descriptor ids (what resolveProseTarget returns).
|
|
2859
|
-
//
|
|
2860
|
-
//
|
|
2894
|
+
// bare descriptor ids (what resolveProseTarget returns). With the server
|
|
2895
|
+
// the speech resolves with, only that server's entry; otherwise scan like
|
|
2896
|
+
// the resolver does, and fail closed on cross-server ambiguity — the
|
|
2897
|
+
// same never-guess rule that governs delivery.
|
|
2861
2898
|
const matches = [...this.channels.values()].filter((e) => e.descriptor.id === channelId);
|
|
2862
|
-
|
|
2899
|
+
const entry = serverId !== undefined
|
|
2900
|
+
? matches.find((e) => e.serverId === serverId)
|
|
2901
|
+
: matches.length === 1 ? matches[0] : undefined;
|
|
2902
|
+
if (!entry)
|
|
2863
2903
|
return null;
|
|
2864
|
-
const server = this.serverRegistry.getServer(
|
|
2904
|
+
const server = this.serverRegistry.getServer(entry.serverId);
|
|
2865
2905
|
// §5.4: the GRANT gates streaming, not the raw advertisement. The old
|
|
2866
2906
|
// `capabilities?.channels?.streaming` check was doubly wrong: undefined
|
|
2867
2907
|
// for the boolean `channels: true` shape (masking discord-mcpl's latent
|
|
@@ -3076,7 +3116,12 @@ export class ChannelRegistry {
|
|
|
3076
3116
|
locusChannelId,
|
|
3077
3117
|
/** `heldOnFailure`: the caller holds a `failed` delivery's words as a
|
|
3078
3118
|
* draft with its own notice, so no failure marker is written for it.
|
|
3079
|
-
* An `unknown` outcome still gets its marker.
|
|
3119
|
+
* An `unknown` outcome still gets its marker. `locusServerId`: the
|
|
3120
|
+
* server the locus resolves with, snapshotted by the caller with the
|
|
3121
|
+
* locus: the server whose message triggered the turn, where the locus is
|
|
3122
|
+
* that message's channel. The speech goes out through that server only,
|
|
3123
|
+
* and fails where it no longer registers the channel. Without it, the
|
|
3124
|
+
* locus is taken only where exactly one server registers it. */
|
|
3080
3125
|
opts) {
|
|
3081
3126
|
const fail = (channelId, reason, outcome) => {
|
|
3082
3127
|
const status = outcome.status === 'unknown' ? 'unknown' : 'failed';
|
|
@@ -3117,7 +3162,11 @@ export class ChannelRegistry {
|
|
|
3117
3162
|
const reason = 'turn has no locus (no home/trigger channel; nothing to deliver into)';
|
|
3118
3163
|
return fail(null, reason, { status: 'failed', reason, at: Date.now() });
|
|
3119
3164
|
}
|
|
3120
|
-
const outcome = await this.publish(conversationId, text, {
|
|
3165
|
+
const outcome = await this.publish(conversationId, text, {
|
|
3166
|
+
...(opts?.locusServerId !== undefined ? { serverId: opts.locusServerId } : {}),
|
|
3167
|
+
channelId: locusChannelId,
|
|
3168
|
+
routed: true,
|
|
3169
|
+
});
|
|
3121
3170
|
const channelId = outcome.destination?.channelId ?? locusChannelId;
|
|
3122
3171
|
if (outcome.status !== 'delivered') {
|
|
3123
3172
|
return fail(channelId, outcome.reason ?? 'delivery not confirmed', outcome);
|
|
@@ -3138,9 +3187,10 @@ export class ChannelRegistry {
|
|
|
3138
3187
|
});
|
|
3139
3188
|
return outcome;
|
|
3140
3189
|
}
|
|
3141
|
-
/** deliverSpeech, reduced to the confirmed delivery (null when there was
|
|
3142
|
-
|
|
3143
|
-
|
|
3190
|
+
/** deliverSpeech, reduced to the confirmed delivery (null when there was
|
|
3191
|
+
* none). `opts.locusServerId` is deliverSpeech's. */
|
|
3192
|
+
async routeSpeech(conversationId, text, locusChannelId, opts) {
|
|
3193
|
+
const outcome = await this.deliverSpeech(conversationId, text, locusChannelId, opts?.locusServerId !== undefined ? { locusServerId: opts.locusServerId } : undefined);
|
|
3144
3194
|
if (outcome.status !== 'delivered')
|
|
3145
3195
|
return null;
|
|
3146
3196
|
return {
|
|
@@ -3173,11 +3223,30 @@ export class ChannelRegistry {
|
|
|
3173
3223
|
isError: true,
|
|
3174
3224
|
};
|
|
3175
3225
|
}
|
|
3176
|
-
|
|
3226
|
+
// A serverId the call gives names the exact pair, as the other channel
|
|
3227
|
+
// tools read theirs (this tool's schema doesn't declare one); with no
|
|
3228
|
+
// channelId, it's the default channel on that server. A call naming no
|
|
3229
|
+
// server takes the calling turn's server for its trigger's or locus's
|
|
3230
|
+
// channel; else, for the default channel, the server whose accepted
|
|
3231
|
+
// message set it; else a channelId only where exactly one server
|
|
3232
|
+
// registers it.
|
|
3233
|
+
const serverId = input.serverId;
|
|
3234
|
+
const trigger = !serverId && origin?.kind === 'agent'
|
|
3235
|
+
? [origin.trigger, origin.locus].find((o) => o?.channelId === channelId)
|
|
3236
|
+
: undefined;
|
|
3237
|
+
const defaultServer = !serverId && trigger === undefined && input.channelId === undefined
|
|
3238
|
+
? this.fallbackServerFor(channelId)
|
|
3239
|
+
: undefined;
|
|
3240
|
+
const resolved = this.resolveToolChannelEntry(channelId, serverId || (trigger?.serverId ?? defaultServer));
|
|
3241
|
+
const entry = resolved.entry;
|
|
3177
3242
|
if (!entry) {
|
|
3178
3243
|
return {
|
|
3179
3244
|
success: false,
|
|
3180
|
-
error:
|
|
3245
|
+
error: trigger !== undefined
|
|
3246
|
+
? `Channel ${channelId} is no longer registered by the server whose message brought you there, so nothing was published.`
|
|
3247
|
+
: defaultServer !== undefined
|
|
3248
|
+
? `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.`
|
|
3249
|
+
: resolved.error,
|
|
3181
3250
|
isError: true,
|
|
3182
3251
|
};
|
|
3183
3252
|
}
|