@animalabs/agent-framework 0.5.6 → 0.6.0
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/agent.d.ts +20 -4
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +45 -14
- package/dist/src/agent.js.map +1 -1
- package/dist/src/framework.d.ts +220 -5
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1489 -69
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +71 -1
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +292 -9
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/gate-script-worker.d.ts +2 -0
- package/dist/src/gate/gate-script-worker.d.ts.map +1 -0
- package/dist/src/gate/gate-script-worker.js +69 -0
- package/dist/src/gate/gate-script-worker.js.map +1 -0
- package/dist/src/gate/gate-script.d.ts +44 -0
- package/dist/src/gate/gate-script.d.ts.map +1 -0
- package/dist/src/gate/gate-script.js +164 -0
- package/dist/src/gate/gate-script.js.map +1 -0
- package/dist/src/gate/types.d.ts +35 -3
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +93 -1
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +260 -6
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.d.ts +7 -0
- package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.js +15 -0
- package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +161 -0
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -0
- package/dist/src/mcpl/conversation-router.js +179 -0
- package/dist/src/mcpl/conversation-router.js.map +1 -0
- package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
- package/dist/src/mcpl/feature-set-manager.js +10 -1
- package/dist/src/mcpl/feature-set-manager.js.map +1 -1
- package/dist/src/mcpl/index.d.ts +2 -0
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js +2 -0
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +1 -0
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +35 -30
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +149 -206
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +98 -0
- package/dist/src/mcpl/transport.d.ts.map +1 -0
- package/dist/src/mcpl/transport.js +281 -0
- package/dist/src/mcpl/transport.js.map +1 -0
- package/dist/src/mcpl/types.d.ts +42 -2
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +5 -1
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +16 -2
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts +10 -0
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +83 -0
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/prose-segments.d.ts +18 -0
- package/dist/src/prose-segments.d.ts.map +1 -0
- package/dist/src/prose-segments.js +39 -0
- package/dist/src/prose-segments.js.map +1 -0
- package/dist/src/runtime/liveness-watchdog-worker.d.ts +2 -0
- package/dist/src/runtime/liveness-watchdog-worker.d.ts.map +1 -0
- package/dist/src/runtime/liveness-watchdog-worker.js +57 -0
- package/dist/src/runtime/liveness-watchdog-worker.js.map +1 -0
- package/dist/src/runtime/liveness-watchdog.d.ts +36 -0
- package/dist/src/runtime/liveness-watchdog.d.ts.map +1 -0
- package/dist/src/runtime/liveness-watchdog.js +106 -0
- package/dist/src/runtime/liveness-watchdog.js.map +1 -0
- package/dist/src/tool-result-history.d.ts +17 -0
- package/dist/src/tool-result-history.d.ts.map +1 -0
- package/dist/src/tool-result-history.js +60 -0
- package/dist/src/tool-result-history.js.map +1 -0
- package/dist/src/types/agent.d.ts +21 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +22 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +27 -0
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +28 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/channel-registry-routing.test.d.ts +2 -0
- package/dist/test/channel-registry-routing.test.d.ts.map +1 -0
- package/dist/test/channel-registry-routing.test.js +182 -0
- package/dist/test/channel-registry-routing.test.js.map +1 -0
- package/dist/test/checkpoint-attribution.test.d.ts +2 -0
- package/dist/test/checkpoint-attribution.test.d.ts.map +1 -0
- package/dist/test/checkpoint-attribution.test.js +40 -0
- package/dist/test/checkpoint-attribution.test.js.map +1 -0
- package/dist/test/conversation-router.test.d.ts +2 -0
- package/dist/test/conversation-router.test.d.ts.map +1 -0
- package/dist/test/conversation-router.test.js +160 -0
- package/dist/test/conversation-router.test.js.map +1 -0
- package/dist/test/conversation-routing.test.d.ts +7 -0
- package/dist/test/conversation-routing.test.d.ts.map +1 -0
- package/dist/test/conversation-routing.test.js +203 -0
- package/dist/test/conversation-routing.test.js.map +1 -0
- package/dist/test/event-gate-policy-mutation.test.d.ts +2 -0
- package/dist/test/event-gate-policy-mutation.test.d.ts.map +1 -0
- package/dist/test/event-gate-policy-mutation.test.js +137 -0
- package/dist/test/event-gate-policy-mutation.test.js.map +1 -0
- package/dist/test/event-gate-script.test.d.ts +2 -0
- package/dist/test/event-gate-script.test.d.ts.map +1 -0
- package/dist/test/event-gate-script.test.js +95 -0
- package/dist/test/event-gate-script.test.js.map +1 -0
- package/dist/test/event-gate-tags.test.d.ts +2 -0
- package/dist/test/event-gate-tags.test.d.ts.map +1 -0
- package/dist/test/event-gate-tags.test.js +76 -0
- package/dist/test/event-gate-tags.test.js.map +1 -0
- package/dist/test/framework.test.js +73 -1
- package/dist/test/framework.test.js.map +1 -1
- package/dist/test/helpers/mock-membrane.d.ts +41 -0
- package/dist/test/helpers/mock-membrane.d.ts.map +1 -0
- package/dist/test/helpers/mock-membrane.js +161 -0
- package/dist/test/helpers/mock-membrane.js.map +1 -0
- package/dist/test/inference-failure-observability.test.d.ts +2 -0
- package/dist/test/inference-failure-observability.test.d.ts.map +1 -0
- package/dist/test/inference-failure-observability.test.js +75 -0
- package/dist/test/inference-failure-observability.test.js.map +1 -0
- package/dist/test/liveness-watchdog.test.d.ts +2 -0
- package/dist/test/liveness-watchdog.test.d.ts.map +1 -0
- package/dist/test/liveness-watchdog.test.js +78 -0
- package/dist/test/liveness-watchdog.test.js.map +1 -0
- package/dist/test/mcpl-ws-transport.test.d.ts +2 -0
- package/dist/test/mcpl-ws-transport.test.d.ts.map +1 -0
- package/dist/test/mcpl-ws-transport.test.js +155 -0
- package/dist/test/mcpl-ws-transport.test.js.map +1 -0
- package/dist/test/prose-segments.test.d.ts +2 -0
- package/dist/test/prose-segments.test.d.ts.map +1 -0
- package/dist/test/prose-segments.test.js +52 -0
- package/dist/test/prose-segments.test.js.map +1 -0
- package/dist/test/tool-result-history.test.d.ts +7 -0
- package/dist/test/tool-result-history.test.d.ts.map +1 -0
- package/dist/test/tool-result-history.test.js +77 -0
- package/dist/test/tool-result-history.test.js.map +1 -0
- package/dist/test/trunk-channel-routing.test.d.ts +19 -0
- package/dist/test/trunk-channel-routing.test.d.ts.map +1 -0
- package/dist/test/trunk-channel-routing.test.js +175 -0
- package/dist/test/trunk-channel-routing.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/agent.ts +53 -18
- package/src/framework.ts +1685 -71
- package/src/gate/event-gate.ts +297 -9
- package/src/gate/gate-script-worker.ts +73 -0
- package/src/gate/gate-script.ts +180 -0
- package/src/gate/types.ts +36 -3
- package/src/index.ts +12 -0
- package/src/mcpl/channel-registry.ts +331 -6
- package/src/mcpl/checkpoint-manager.ts +16 -0
- package/src/mcpl/conversation-router.ts +284 -0
- package/src/mcpl/feature-set-manager.ts +14 -1
- package/src/mcpl/index.ts +12 -0
- package/src/mcpl/push-handler.ts +1 -0
- package/src/mcpl/server-connection.ts +157 -224
- package/src/mcpl/transport.ts +307 -0
- package/src/mcpl/types.ts +49 -2
- package/src/module-registry.ts +25 -2
- package/src/modules/discord/index.ts +93 -0
- package/src/prose-segments.ts +42 -0
- package/src/runtime/liveness-watchdog-worker.ts +66 -0
- package/src/runtime/liveness-watchdog.ts +122 -0
- package/src/tool-result-history.ts +57 -0
- package/src/types/agent.ts +21 -0
- package/src/types/framework.ts +24 -0
- package/src/types/module.ts +29 -0
- package/src/types/trace.ts +36 -0
|
@@ -174,6 +174,43 @@ const CHANNEL_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
174
174
|
required: [],
|
|
175
175
|
},
|
|
176
176
|
},
|
|
177
|
+
{
|
|
178
|
+
name: 'think',
|
|
179
|
+
description:
|
|
180
|
+
'Reason privately. The content stays in your own context and is NOT sent to any ' +
|
|
181
|
+
'channel or surface. This does NOT end or silence your turn: if you also write ' +
|
|
182
|
+
'plain text this turn, that text is still posted as your reply. Use think() purely ' +
|
|
183
|
+
'to work things out before (or instead of) speaking. To deliberately NOT reply this ' +
|
|
184
|
+
'turn, call skip_reply instead.',
|
|
185
|
+
inputSchema: {
|
|
186
|
+
type: 'object' as const,
|
|
187
|
+
properties: {
|
|
188
|
+
content: {
|
|
189
|
+
type: 'string',
|
|
190
|
+
description: 'Your private thought / reasoning (optional; not sent anywhere).',
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
required: [],
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: 'skip_reply',
|
|
198
|
+
description:
|
|
199
|
+
'End your turn WITHOUT sending anything to any channel or surface. Use when you have ' +
|
|
200
|
+
'read the messages but deliberately choose not to reply right now — ambient chatter, ' +
|
|
201
|
+
'nothing to add, or you are waiting. Any plain text you wrote this turn stays private ' +
|
|
202
|
+
'and is NOT posted. To reply instead, just write plain text (no tool call).',
|
|
203
|
+
inputSchema: {
|
|
204
|
+
type: 'object' as const,
|
|
205
|
+
properties: {
|
|
206
|
+
reason: {
|
|
207
|
+
type: 'string',
|
|
208
|
+
description: 'Optional private note on why you are not replying (not sent anywhere).',
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
required: [],
|
|
212
|
+
},
|
|
213
|
+
},
|
|
177
214
|
];
|
|
178
215
|
|
|
179
216
|
// ============================================================================
|
|
@@ -183,6 +220,45 @@ const CHANNEL_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
183
220
|
interface ChannelRegistryOptions {
|
|
184
221
|
/** Callback to determine whether an incoming message should trigger inference. */
|
|
185
222
|
shouldTriggerInference?: (content: string, metadata: Record<string, unknown>) => boolean;
|
|
223
|
+
/**
|
|
224
|
+
* Called when a text-only turn's speech could NOT be delivered to its
|
|
225
|
+
* conversational locus — no locus, an unregistered channel, a missing
|
|
226
|
+
* server, or the server reporting `delivered: false`. The host wires this
|
|
227
|
+
* to drop a `[discord-send-failed]` marker into chronicle so the failure is
|
|
228
|
+
* visible to the agent (and operator) rather than silently lost. Must not
|
|
229
|
+
* itself trigger inference (avoid wake loops).
|
|
230
|
+
*/
|
|
231
|
+
onRouteFailure?: (info: {
|
|
232
|
+
conversationId: string;
|
|
233
|
+
channelId: string | null;
|
|
234
|
+
reason: string;
|
|
235
|
+
textLen: number;
|
|
236
|
+
}) => void;
|
|
237
|
+
/**
|
|
238
|
+
* Resolve a conversation fork's HOME channel from its agent name. Conversation
|
|
239
|
+
* forks are spawned bound to a single channel (the framework tracks this in
|
|
240
|
+
* `conversationAgentHomes` / `ConversationRouter.channelForAgent`); their
|
|
241
|
+
* plain-text speech must route THERE. The process-global `defaultPublishChannel`
|
|
242
|
+
* tracks only the most-recent inbound across ALL channels, so with one fork per
|
|
243
|
+
* channel running concurrently it misroutes a fork's reply to whichever channel
|
|
244
|
+
* last spoke (item 3). Returns undefined for the trunk/primary agent, which has
|
|
245
|
+
* no home and correctly falls back to the global locus (heartbeats, etc.).
|
|
246
|
+
*/
|
|
247
|
+
homeChannelResolver?: (agentName: string) => string | undefined;
|
|
248
|
+
/**
|
|
249
|
+
* Resolve the channel that triggered an agent's CURRENT inference turn, by
|
|
250
|
+
* agent name. This is the fix for single-TRUNK agents (the only mode
|
|
251
|
+
* connectome-host runs — it never exposes conversation forks). A trunk agent
|
|
252
|
+
* has no fork home, so without this its plain-text speech falls back to the
|
|
253
|
+
* process-global `defaultPublishChannel`, which tracks the most-recent inbound
|
|
254
|
+
* across ALL channels and misroutes a reply to whichever channel last spoke
|
|
255
|
+
* under concurrency (item-3 redux). The framework tracks the triggering
|
|
256
|
+
* channel of the live turn per-agent and exposes it here; resolves to
|
|
257
|
+
* undefined for a heartbeat / no-trigger turn, which correctly keeps the
|
|
258
|
+
* global fallback. Consulted AFTER `homeChannelResolver` (a fork's home always
|
|
259
|
+
* wins), BEFORE `defaultPublishChannel`.
|
|
260
|
+
*/
|
|
261
|
+
activeChannelResolver?: (agentName: string) => string | undefined;
|
|
186
262
|
}
|
|
187
263
|
|
|
188
264
|
// ============================================================================
|
|
@@ -201,6 +277,14 @@ export class ChannelRegistry {
|
|
|
201
277
|
op?: 'start' | 'stop',
|
|
202
278
|
) => void;
|
|
203
279
|
private shouldTriggerInference?: (content: string, metadata: Record<string, unknown>) => boolean;
|
|
280
|
+
private onRouteFailure?: (info: {
|
|
281
|
+
conversationId: string;
|
|
282
|
+
channelId: string | null;
|
|
283
|
+
reason: string;
|
|
284
|
+
textLen: number;
|
|
285
|
+
}) => void;
|
|
286
|
+
private homeChannelResolver?: (agentName: string) => string | undefined;
|
|
287
|
+
private activeChannelResolver?: (agentName: string) => string | undefined;
|
|
204
288
|
|
|
205
289
|
/** Registered channels, keyed by `{serverId}:{channelId}`. */
|
|
206
290
|
private channels = new Map<string, ChannelEntry>();
|
|
@@ -246,6 +330,9 @@ export class ChannelRegistry {
|
|
|
246
330
|
this.emitTraceFn = emitTraceFn;
|
|
247
331
|
this.sendTypingFn = options?.sendTypingFn;
|
|
248
332
|
this.shouldTriggerInference = options?.shouldTriggerInference;
|
|
333
|
+
this.onRouteFailure = options?.onRouteFailure;
|
|
334
|
+
this.homeChannelResolver = options?.homeChannelResolver;
|
|
335
|
+
this.activeChannelResolver = options?.activeChannelResolver;
|
|
249
336
|
}
|
|
250
337
|
|
|
251
338
|
/**
|
|
@@ -372,6 +459,43 @@ export class ChannelRegistry {
|
|
|
372
459
|
this.defaultPublishMessageId = message.messageId;
|
|
373
460
|
this.defaultPublishThreadId = message.threadId;
|
|
374
461
|
|
|
462
|
+
// Lazy-register the channel if we've never seen it. A channel can deliver
|
|
463
|
+
// an incoming message before its channels/register (boot enumeration) or
|
|
464
|
+
// channels/changed (post-boot create / View-permission grant) round-trip
|
|
465
|
+
// lands — or the registration event can be missed entirely (e.g. the bot
|
|
466
|
+
// gains visibility in a way that fires neither `channelCreate` nor a
|
|
467
|
+
// View-permission transition). Without a registry entry, routeSpeech()
|
|
468
|
+
// can't resolve this channel as an outbound locus and the agent's reply
|
|
469
|
+
// is silently dropped, even though this very message proves the channel
|
|
470
|
+
// is reachable. The inbound message carries enough to make it publishable,
|
|
471
|
+
// so register it here; a later authoritative channels/register or
|
|
472
|
+
// channels/changed will overwrite this descriptor with the richer one.
|
|
473
|
+
const incomingKey = `${serverId}:${message.channelId}`;
|
|
474
|
+
if (!this.channels.has(incomingKey)) {
|
|
475
|
+
const channelLabel =
|
|
476
|
+
typeof message.metadata?.channelName === 'string' && message.metadata.channelName
|
|
477
|
+
? (message.metadata.channelName as string)
|
|
478
|
+
: message.channelId;
|
|
479
|
+
this.channels.set(incomingKey, {
|
|
480
|
+
serverId,
|
|
481
|
+
descriptor: {
|
|
482
|
+
id: message.channelId,
|
|
483
|
+
type: serverId,
|
|
484
|
+
label: channelLabel,
|
|
485
|
+
direction: 'bidirectional',
|
|
486
|
+
address: message.threadId ? { threadId: message.threadId } : undefined,
|
|
487
|
+
metadata: { lazyRegistered: true },
|
|
488
|
+
},
|
|
489
|
+
open: true,
|
|
490
|
+
});
|
|
491
|
+
this.emitTraceFn({
|
|
492
|
+
type: 'mcpl:channel-lazy-registered',
|
|
493
|
+
serverId,
|
|
494
|
+
channelId: message.channelId,
|
|
495
|
+
label: channelLabel,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
375
499
|
// Determine whether to trigger inference
|
|
376
500
|
let triggerInference = true;
|
|
377
501
|
if (this.shouldTriggerInference) {
|
|
@@ -389,6 +513,7 @@ export class ChannelRegistry {
|
|
|
389
513
|
messageId: message.messageId,
|
|
390
514
|
threadId: message.threadId,
|
|
391
515
|
author: message.author,
|
|
516
|
+
...(message.tags ? { tags: message.tags } : {}),
|
|
392
517
|
},
|
|
393
518
|
);
|
|
394
519
|
}
|
|
@@ -430,6 +555,50 @@ export class ChannelRegistry {
|
|
|
430
555
|
});
|
|
431
556
|
}
|
|
432
557
|
|
|
558
|
+
/**
|
|
559
|
+
* Ensure a channel is registered AND open so it can serve as an outbound
|
|
560
|
+
* routing locus, lazy-registering it if unseen and opening it if closed.
|
|
561
|
+
*
|
|
562
|
+
* Mirrors the lazy-registration inside handleIncoming(), but for channels
|
|
563
|
+
* that only ever arrive as push/events rather than channels/incoming — the
|
|
564
|
+
* motivating case is Discord DMs, which discord-mcpl forwards via push/event
|
|
565
|
+
* with the channel closed (`channelIsOpen:false`). Such a channel is never
|
|
566
|
+
* registered, so routeSpeech() can't resolve it and the agent's reply is
|
|
567
|
+
* silently dropped even though the inbound message proves the channel is
|
|
568
|
+
* reachable (item-3 redux, DM sub-case). Idempotent: a later authoritative
|
|
569
|
+
* channels/register or channels/changed overwrites this descriptor with the
|
|
570
|
+
* richer one; re-opening an already-open channel is a no-op.
|
|
571
|
+
*/
|
|
572
|
+
ensureChannelRegistered(serverId: string, channelId: string, label?: string): void {
|
|
573
|
+
const existing = this.findChannelEntry(channelId);
|
|
574
|
+
if (existing) {
|
|
575
|
+
if (!existing.open) {
|
|
576
|
+
existing.open = true;
|
|
577
|
+
this.emitTraceFn({ type: 'mcpl:channel-opened', serverId: existing.serverId, channelId });
|
|
578
|
+
}
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const key = `${serverId}:${channelId}`;
|
|
583
|
+
this.channels.set(key, {
|
|
584
|
+
serverId,
|
|
585
|
+
descriptor: {
|
|
586
|
+
id: channelId,
|
|
587
|
+
type: serverId,
|
|
588
|
+
label: label ?? channelId,
|
|
589
|
+
direction: 'bidirectional',
|
|
590
|
+
metadata: { lazyRegistered: true },
|
|
591
|
+
},
|
|
592
|
+
open: true,
|
|
593
|
+
});
|
|
594
|
+
this.emitTraceFn({
|
|
595
|
+
type: 'mcpl:channel-lazy-registered',
|
|
596
|
+
serverId,
|
|
597
|
+
channelId,
|
|
598
|
+
label: label ?? channelId,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
|
|
433
602
|
// ==========================================================================
|
|
434
603
|
// Typing Indicator Management
|
|
435
604
|
// ==========================================================================
|
|
@@ -536,6 +705,14 @@ export class ChannelRegistry {
|
|
|
536
705
|
return this.defaultPublishChannel;
|
|
537
706
|
}
|
|
538
707
|
|
|
708
|
+
/**
|
|
709
|
+
* Get the descriptor for a channel by its channelId (first match across
|
|
710
|
+
* servers). Used by the conversation router for DM classification.
|
|
711
|
+
*/
|
|
712
|
+
getDescriptor(channelId: string): ChannelDescriptor | undefined {
|
|
713
|
+
return this.findChannelEntry(channelId)?.descriptor;
|
|
714
|
+
}
|
|
715
|
+
|
|
539
716
|
/**
|
|
540
717
|
* Get all open channels.
|
|
541
718
|
*/
|
|
@@ -577,6 +754,12 @@ export class ChannelRegistry {
|
|
|
577
754
|
case 'channel_publish':
|
|
578
755
|
return this.handleToolPublish(input as { channelId: string; content: string });
|
|
579
756
|
|
|
757
|
+
case 'think':
|
|
758
|
+
return this.handleToolThink(input as { content?: string });
|
|
759
|
+
|
|
760
|
+
case 'skip_reply':
|
|
761
|
+
return this.handleToolSkipReply(input as { reason?: string });
|
|
762
|
+
|
|
580
763
|
default:
|
|
581
764
|
return { success: false, error: `Unknown channel tool: ${toolName}`, isError: true };
|
|
582
765
|
}
|
|
@@ -591,15 +774,28 @@ export class ChannelRegistry {
|
|
|
591
774
|
*
|
|
592
775
|
* Returns undefined if no channels are active.
|
|
593
776
|
*/
|
|
594
|
-
buildChannelContext(): ChannelContext | undefined {
|
|
777
|
+
buildChannelContext(agentName?: string): ChannelContext | undefined {
|
|
595
778
|
const openChannels = this.getOpenChannels();
|
|
596
|
-
|
|
779
|
+
|
|
780
|
+
// Resolve the outbound locus the SAME way routeSpeech does, so the agent is
|
|
781
|
+
// told the channel its speech will actually land in: a conversation fork's
|
|
782
|
+
// home channel, else this turn's triggering channel (single trunk agent —
|
|
783
|
+
// item-3 redux), else the global default (heartbeats). Without this, a fork
|
|
784
|
+
// was advertised the global locus but published somewhere else; and a trunk
|
|
785
|
+
// agent was told the wrong channel under concurrency.
|
|
786
|
+
const home = agentName ? this.homeChannelResolver?.(agentName) : undefined;
|
|
787
|
+
const active = agentName ? this.activeChannelResolver?.(agentName) : undefined;
|
|
788
|
+
const outgoing = home ?? active ?? this.defaultPublishChannel;
|
|
789
|
+
|
|
790
|
+
if (openChannels.length === 0 && !outgoing) {
|
|
597
791
|
return undefined;
|
|
598
792
|
}
|
|
599
793
|
|
|
600
794
|
const context: ChannelContext = {};
|
|
601
795
|
|
|
602
|
-
// Incoming:
|
|
796
|
+
// Incoming: the most-recent inbound message (what the agent is replying to).
|
|
797
|
+
// Left process-global — per-channel inbound tracking (the right messageId for
|
|
798
|
+
// a fork's own channel) is a separate concern from the outbound routing fix.
|
|
603
799
|
if (this.defaultPublishChannel && this.defaultPublishMessageId) {
|
|
604
800
|
context.incoming = {
|
|
605
801
|
channelId: this.defaultPublishChannel,
|
|
@@ -608,10 +804,10 @@ export class ChannelRegistry {
|
|
|
608
804
|
};
|
|
609
805
|
}
|
|
610
806
|
|
|
611
|
-
// Default outgoing:
|
|
612
|
-
if (
|
|
807
|
+
// Default outgoing: the resolved outbound locus (home channel for forks).
|
|
808
|
+
if (outgoing) {
|
|
613
809
|
context.defaultOutgoing = {
|
|
614
|
-
channelId:
|
|
810
|
+
channelId: outgoing,
|
|
615
811
|
};
|
|
616
812
|
}
|
|
617
813
|
|
|
@@ -730,6 +926,11 @@ export class ChannelRegistry {
|
|
|
730
926
|
// Private: Channel Lookup
|
|
731
927
|
// ==========================================================================
|
|
732
928
|
|
|
929
|
+
/** Server id owning a registered channel (by descriptor id), or null. */
|
|
930
|
+
getChannelServerId(channelId: string): string | null {
|
|
931
|
+
return this.findChannelEntry(channelId)?.serverId ?? null;
|
|
932
|
+
}
|
|
933
|
+
|
|
733
934
|
/**
|
|
734
935
|
* Find a channel entry by channelId (searches across all servers).
|
|
735
936
|
* Returns the first match.
|
|
@@ -874,6 +1075,130 @@ export class ChannelRegistry {
|
|
|
874
1075
|
}
|
|
875
1076
|
}
|
|
876
1077
|
|
|
1078
|
+
/**
|
|
1079
|
+
* Handle the synthesized `think` tool — a private reasoning scratchpad. It
|
|
1080
|
+
* sends nothing, and (unlike before) does NOT silence the turn: trailing
|
|
1081
|
+
* plain text is still routed as the reply. The thought stays in the agent's
|
|
1082
|
+
* own context/chronicle. To deliberately not reply, the agent uses skip_reply.
|
|
1083
|
+
*/
|
|
1084
|
+
private handleToolThink(input: { content?: string }): ToolResult {
|
|
1085
|
+
return {
|
|
1086
|
+
success: true,
|
|
1087
|
+
data: {
|
|
1088
|
+
noted: true,
|
|
1089
|
+
note:
|
|
1090
|
+
'Thought recorded (private — not sent anywhere). This did NOT silence your turn: ' +
|
|
1091
|
+
'write plain text to reply, or call skip_reply to end the turn without replying.',
|
|
1092
|
+
content: typeof input?.content === 'string' ? input.content : undefined,
|
|
1093
|
+
},
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* Handle the synthesized `skip_reply` tool — the deliberate "stay silent"
|
|
1099
|
+
* signal. A no-op as far as any surface is concerned (sends nothing); its
|
|
1100
|
+
* effect is that the framework's output routing treats this as a silencing
|
|
1101
|
+
* tool, so any trailing prose this turn is NOT posted. Replaces the old
|
|
1102
|
+
* overloaded use of `think` for staying silent.
|
|
1103
|
+
*/
|
|
1104
|
+
private handleToolSkipReply(input: { reason?: string }): ToolResult {
|
|
1105
|
+
return {
|
|
1106
|
+
success: true,
|
|
1107
|
+
data: {
|
|
1108
|
+
skipped: true,
|
|
1109
|
+
note: 'Ended the turn without replying — nothing was sent to any channel.',
|
|
1110
|
+
reason: typeof input?.reason === 'string' ? input.reason : undefined,
|
|
1111
|
+
},
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/**
|
|
1116
|
+
* Host-owned output routing (see forking-knowledge-miner LOCUS-ROUTING-DESIGN).
|
|
1117
|
+
* Publish the agent's plain-text speech to the current conversational locus
|
|
1118
|
+
* (the most recent incoming channel, tracked cross-surface here in the host).
|
|
1119
|
+
* Called by the framework on a text-only turn — replaces the per-surface
|
|
1120
|
+
* sticky auto-post that used to live in discord-mcpl. Returns null when there
|
|
1121
|
+
* is no locus / the channel or its server can't be resolved (in which case
|
|
1122
|
+
* the speech simply stays in chronicle + module surfaces).
|
|
1123
|
+
*/
|
|
1124
|
+
async routeSpeech(
|
|
1125
|
+
conversationId: string,
|
|
1126
|
+
text: string,
|
|
1127
|
+
): Promise<{ delivered: boolean; channelId: string } | null> {
|
|
1128
|
+
// Surface a routing failure: emit a trace AND notify the host (which drops
|
|
1129
|
+
// a `[discord-send-failed]` marker into chronicle) so the agent learns her
|
|
1130
|
+
// reply never reached the human, instead of it vanishing silently.
|
|
1131
|
+
const fail = (channelId: string | null, reason: string): null => {
|
|
1132
|
+
console.error(`[routeSpeech] ${conversationId}: ${reason} — speech NOT routed (${text.length} chars stay in chronicle)`);
|
|
1133
|
+
this.emitTraceFn({
|
|
1134
|
+
type: 'mcpl:speech-route-failed',
|
|
1135
|
+
channelId: channelId ?? '',
|
|
1136
|
+
reason,
|
|
1137
|
+
textLen: text.length,
|
|
1138
|
+
});
|
|
1139
|
+
this.onRouteFailure?.({ conversationId, channelId, reason, textLen: text.length });
|
|
1140
|
+
return null;
|
|
1141
|
+
};
|
|
1142
|
+
|
|
1143
|
+
// Resolve the outbound locus, in precedence order:
|
|
1144
|
+
// 1. fork HOME channel — a conversation fork always replies in the channel
|
|
1145
|
+
// it was spawned to serve (item 3, forks).
|
|
1146
|
+
// 2. this turn's TRIGGERING channel — a single trunk agent (connectome-
|
|
1147
|
+
// host's only mode) replies in the channel whose message woke THIS
|
|
1148
|
+
// inference, so a concurrent inbound elsewhere can't hijack the reply
|
|
1149
|
+
// (item-3 redux, trunk agents). Also carries DM channels, which arrive
|
|
1150
|
+
// as push/events and never touch `defaultPublishChannel`.
|
|
1151
|
+
// 3. the process-global `defaultPublishChannel` — last resort for turns
|
|
1152
|
+
// with no triggering channel (heartbeats, timers).
|
|
1153
|
+
// Using the global for a fork or a concurrent trunk turn is the item-3 bug:
|
|
1154
|
+
// it tracks the most-recent inbound across ALL channels, so a reply lands
|
|
1155
|
+
// wherever a message last happened to arrive rather than where it belongs.
|
|
1156
|
+
const home = this.homeChannelResolver?.(conversationId);
|
|
1157
|
+
const channelId =
|
|
1158
|
+
home ?? this.activeChannelResolver?.(conversationId) ?? this.defaultPublishChannel;
|
|
1159
|
+
if (!channelId) {
|
|
1160
|
+
// Reached only when the agent has no home, no active triggering channel,
|
|
1161
|
+
// AND no global inbound was ever seen.
|
|
1162
|
+
return fail(null, 'no locus (no home/active channel, defaultPublishChannel null)');
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
const entry = this.findChannelEntry(channelId);
|
|
1166
|
+
if (!entry) {
|
|
1167
|
+
return fail(channelId, `no registered channel for locus "${channelId}"`);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
const server = this.serverRegistry.getServer(entry.serverId);
|
|
1171
|
+
if (!server) {
|
|
1172
|
+
return fail(channelId, `server "${entry.serverId}" not found`);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
const publishParams: ChannelsPublishParams = {
|
|
1176
|
+
conversationId,
|
|
1177
|
+
channelId,
|
|
1178
|
+
content: [{ type: 'text', text }],
|
|
1179
|
+
};
|
|
1180
|
+
const result = await server.sendChannelsPublish(publishParams);
|
|
1181
|
+
const delivered = (result as { delivered?: boolean } | undefined)?.delivered ?? true;
|
|
1182
|
+
|
|
1183
|
+
// The server accepted the publish RPC but reported the message was not
|
|
1184
|
+
// actually delivered (e.g. missing Send Messages permission). Previously
|
|
1185
|
+
// this returned `{ delivered: true }`, masking the failure. Surface it.
|
|
1186
|
+
if (delivered === false) {
|
|
1187
|
+
return fail(channelId, `server "${entry.serverId}" reported delivered:false for "${channelId}"`);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
console.error(`[routeSpeech] ${conversationId}: routed ${text.length} chars -> ${channelId} (server=${entry.serverId}, delivered=${delivered})`);
|
|
1191
|
+
this.emitTraceFn({
|
|
1192
|
+
type: 'mcpl:speech-routed',
|
|
1193
|
+
serverId: entry.serverId,
|
|
1194
|
+
channelId,
|
|
1195
|
+
delivered,
|
|
1196
|
+
textLen: text.length,
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
return { delivered, channelId };
|
|
1200
|
+
}
|
|
1201
|
+
|
|
877
1202
|
private async handleToolPublish(input: { channelId?: string; content?: string; text?: string }): Promise<ToolResult> {
|
|
878
1203
|
// Resolve content: accept both `content` and `text` (backward compat)
|
|
879
1204
|
const messageText = input.content ?? input.text;
|
|
@@ -368,6 +368,22 @@ export class CheckpointManager {
|
|
|
368
368
|
return null;
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
+
/**
|
|
372
|
+
* Get the server's host-managed (hostState) feature set. Host-managed state is
|
|
373
|
+
* single-valued by protocol (one unkeyed `state` per tool call), so there is at
|
|
374
|
+
* most one such set — return it so its state is threaded correctly even when the
|
|
375
|
+
* server also declares server-managed (rollback) sets. Returns null if none.
|
|
376
|
+
*/
|
|
377
|
+
getHostManagedFeatureSet(serverId: string): string | null {
|
|
378
|
+
const prefix = `${serverId}:`;
|
|
379
|
+
for (const [key, tree] of this.trees) {
|
|
380
|
+
if (key.startsWith(prefix) && tree.hostState) {
|
|
381
|
+
return key.slice(prefix.length);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
|
|
371
387
|
// ==========================================================================
|
|
372
388
|
// Rollback
|
|
373
389
|
// ==========================================================================
|