@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
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConversationRouter — routes incoming channel messages to per-channel
|
|
3
|
+
* conversation agents ("forks") instead of the primary conversation.
|
|
4
|
+
*
|
|
5
|
+
* The router is a lookup table plus policy — deliberately no LLM in the
|
|
6
|
+
* routing path. The framework consults it from handleMcplChannelIncoming:
|
|
7
|
+
*
|
|
8
|
+
* - A channel with an active binding routes to its bound agent. Whether the
|
|
9
|
+
* message *triggers* inference (vs. just landing in the fork's context) is
|
|
10
|
+
* a separate per-channel-kind rule: in a busy channel everything is case
|
|
11
|
+
* input but only mentions demand a reply.
|
|
12
|
+
* - An unbound channel binds (→ the framework spawns a fork from the
|
|
13
|
+
* template/trunk agent) when the bind rule matches: by default any message
|
|
14
|
+
* in a DM, an explicit bot mention in a channel (`metadata.mentioned`,
|
|
15
|
+
* set server-side by the platform adapters).
|
|
16
|
+
* - Everything else is left unrouted and dropped by the framework — the
|
|
17
|
+
* trunk is a dormant template, not a listener.
|
|
18
|
+
*
|
|
19
|
+
* Bindings expire after an idle TTL. Expiry runs a final closure turn on the
|
|
20
|
+
* fork (see framework wiring) and unbinds; the next qualifying message on
|
|
21
|
+
* that channel spawns a *fresh* fork (generation + 1) from the current
|
|
22
|
+
* trunk, so handbook/template updates propagate between engagements.
|
|
23
|
+
*
|
|
24
|
+
* The binding table is in-memory — bindings are re-established by the next
|
|
25
|
+
* qualifying message after a restart. The generation counters, by contrast,
|
|
26
|
+
* are persisted by the framework (exportGenerations/hydrateGenerations):
|
|
27
|
+
* a restarted host must NOT reuse generation 1's agent name, or it would
|
|
28
|
+
* reopen the previous engagement's Chronicle namespace and re-seed the
|
|
29
|
+
* template context on top of the old history.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import type { ContextStrategy } from '@animalabs/context-manager';
|
|
33
|
+
import type { ChannelDescriptor } from './types.js';
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Config & types
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
/** When an unbound channel acquires a fork. */
|
|
40
|
+
export type BindRule = 'always' | 'mention' | 'never';
|
|
41
|
+
|
|
42
|
+
/** When a message on a bound channel triggers inference (it always lands in
|
|
43
|
+
* the fork's context). */
|
|
44
|
+
export type TriggerRule = 'always' | 'mention';
|
|
45
|
+
|
|
46
|
+
/** How a channel is classified for bind/trigger rules. Group DMs are their
|
|
47
|
+
* own kind: the bot was added deliberately (so they bind like DMs) but they
|
|
48
|
+
* host multi-human chatter (so they trigger like channels by default —
|
|
49
|
+
* inferring on every message of a four-human group DM is a firehose). */
|
|
50
|
+
export type ChannelKind = 'dm' | 'groupDm' | 'channel';
|
|
51
|
+
|
|
52
|
+
export interface ConversationRouterConfig {
|
|
53
|
+
/** Agent whose context seeds new forks (the "trunk"/warm checkpoint). */
|
|
54
|
+
templateAgent: string;
|
|
55
|
+
|
|
56
|
+
/** Bind rules per channel kind.
|
|
57
|
+
* Defaults: dm 'always', groupDm 'always', channel 'mention'. */
|
|
58
|
+
bind?: { dm?: BindRule; groupDm?: BindRule; channel?: BindRule };
|
|
59
|
+
|
|
60
|
+
/** Trigger rules per channel kind.
|
|
61
|
+
* Defaults: dm 'always', groupDm 'mention', channel 'mention'. */
|
|
62
|
+
trigger?: { dm?: TriggerRule; groupDm?: TriggerRule; channel?: TriggerRule };
|
|
63
|
+
|
|
64
|
+
/** Idle time before a binding expires and the fork is closed. Default 12h. */
|
|
65
|
+
idleTtlMs?: number;
|
|
66
|
+
|
|
67
|
+
/** Final system-initiated user message sent to a fork on expiry. */
|
|
68
|
+
closurePrompt?: string;
|
|
69
|
+
|
|
70
|
+
/** Prefix for generated fork agent names. Default 'conversation'. */
|
|
71
|
+
agentPrefix?: string;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Fresh compression strategy per fork. Strategy instances are stateful and
|
|
75
|
+
* must not be shared between ContextManagers, so the template agent's own
|
|
76
|
+
* strategy is never reused — without a factory, forks get passthrough.
|
|
77
|
+
*/
|
|
78
|
+
strategyFactory?: () => ContextStrategy;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ConversationBinding {
|
|
82
|
+
channelId: string;
|
|
83
|
+
agentName: string;
|
|
84
|
+
/** Engagement counter per channel — fresh forks get a new generation. */
|
|
85
|
+
generation: number;
|
|
86
|
+
boundAt: number;
|
|
87
|
+
lastActivity: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The facts route() needs about one incoming message, pre-extracted by the
|
|
91
|
+
* framework so the router stays free of event-shape knowledge. */
|
|
92
|
+
export interface IncomingMessageFacts {
|
|
93
|
+
channelId: string;
|
|
94
|
+
/** Personal mention of the bot (metadata.mentioned from the adapter). */
|
|
95
|
+
mentioned: boolean;
|
|
96
|
+
/** Channel classification (see classifyChannel). */
|
|
97
|
+
kind: ChannelKind;
|
|
98
|
+
/** Clock injection for tests; defaults to Date.now(). */
|
|
99
|
+
now?: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type RoutingDecision =
|
|
103
|
+
/** Channel already has a fork — deliver there. */
|
|
104
|
+
| { kind: 'existing'; agentName: string; trigger: boolean }
|
|
105
|
+
/** Bind rule matched — framework should spawn this agent, then bind(). */
|
|
106
|
+
| { kind: 'spawn'; agentName: string; generation: number; trigger: boolean }
|
|
107
|
+
/** No binding and bind rule didn't match — drop. */
|
|
108
|
+
| { kind: 'unbound' };
|
|
109
|
+
|
|
110
|
+
const DEFAULT_IDLE_TTL_MS = 12 * 60 * 60 * 1000;
|
|
111
|
+
|
|
112
|
+
export const DEFAULT_CLOSURE_PROMPT =
|
|
113
|
+
'[system] This engagement is closing due to inactivity. Finalize your ' +
|
|
114
|
+
'work: post any promised results to the channel and record outstanding ' +
|
|
115
|
+
'threads as unresolved. Do not ask follow-up questions.';
|
|
116
|
+
|
|
117
|
+
// ============================================================================
|
|
118
|
+
// ConversationRouter
|
|
119
|
+
// ============================================================================
|
|
120
|
+
|
|
121
|
+
export class ConversationRouter {
|
|
122
|
+
private config: ConversationRouterConfig;
|
|
123
|
+
|
|
124
|
+
/** Active bindings, keyed by channelId. */
|
|
125
|
+
private bindings = new Map<string, ConversationBinding>();
|
|
126
|
+
|
|
127
|
+
/** Engagement counter per channel — survives unbind, so a re-bound channel
|
|
128
|
+
* gets a distinct fork agent name. */
|
|
129
|
+
private generations = new Map<string, number>();
|
|
130
|
+
|
|
131
|
+
constructor(config: ConversationRouterConfig) {
|
|
132
|
+
this.config = config;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
get templateAgent(): string {
|
|
136
|
+
return this.config.templateAgent;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
get closurePrompt(): string {
|
|
140
|
+
return this.config.closurePrompt ?? DEFAULT_CLOSURE_PROMPT;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
get idleTtlMs(): number {
|
|
144
|
+
return this.config.idleTtlMs ?? DEFAULT_IDLE_TTL_MS;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
get strategyFactory(): (() => ContextStrategy) | undefined {
|
|
148
|
+
return this.config.strategyFactory;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Decide where an incoming message goes. Pure read except for refreshing
|
|
153
|
+
* lastActivity on an existing binding — a 'spawn' decision does NOT create
|
|
154
|
+
* the binding; the framework calls bind() after the fork agent actually
|
|
155
|
+
* exists, so a failed spawn doesn't leave a dangling route.
|
|
156
|
+
*/
|
|
157
|
+
route(facts: IncomingMessageFacts): RoutingDecision {
|
|
158
|
+
const now = facts.now ?? Date.now();
|
|
159
|
+
const existing = this.bindings.get(facts.channelId);
|
|
160
|
+
|
|
161
|
+
if (existing) {
|
|
162
|
+
existing.lastActivity = now;
|
|
163
|
+
return {
|
|
164
|
+
kind: 'existing',
|
|
165
|
+
agentName: existing.agentName,
|
|
166
|
+
trigger: this.shouldTrigger(facts),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const bindRule = this.config.bind?.[facts.kind]
|
|
171
|
+
?? (facts.kind === 'channel' ? 'mention' : 'always');
|
|
172
|
+
|
|
173
|
+
const binds =
|
|
174
|
+
bindRule === 'always' || (bindRule === 'mention' && facts.mentioned);
|
|
175
|
+
if (!binds) {
|
|
176
|
+
return { kind: 'unbound' };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const generation = (this.generations.get(facts.channelId) ?? 0) + 1;
|
|
180
|
+
return {
|
|
181
|
+
kind: 'spawn',
|
|
182
|
+
agentName: this.forkAgentName(facts.channelId, generation),
|
|
183
|
+
generation,
|
|
184
|
+
trigger: this.shouldTrigger(facts),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Record a binding after the framework successfully spawned the fork.
|
|
190
|
+
* Returns the new binding.
|
|
191
|
+
*/
|
|
192
|
+
bind(channelId: string, agentName: string, generation: number, now = Date.now()): ConversationBinding {
|
|
193
|
+
this.generations.set(channelId, generation);
|
|
194
|
+
const binding: ConversationBinding = {
|
|
195
|
+
channelId,
|
|
196
|
+
agentName,
|
|
197
|
+
generation,
|
|
198
|
+
boundAt: now,
|
|
199
|
+
lastActivity: now,
|
|
200
|
+
};
|
|
201
|
+
this.bindings.set(channelId, binding);
|
|
202
|
+
return binding;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
unbind(channelId: string): void {
|
|
206
|
+
this.bindings.delete(channelId);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
getBinding(channelId: string): ConversationBinding | undefined {
|
|
210
|
+
return this.bindings.get(channelId);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Reverse lookup: the channel an agent is bound to, if any. */
|
|
214
|
+
channelForAgent(agentName: string): string | undefined {
|
|
215
|
+
for (const binding of this.bindings.values()) {
|
|
216
|
+
if (binding.agentName === agentName) return binding.channelId;
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
getBindings(): ConversationBinding[] {
|
|
222
|
+
return Array.from(this.bindings.values());
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Bindings whose idle TTL has elapsed. Does not unbind — the framework
|
|
226
|
+
* runs the closure turn first, then calls unbind(). */
|
|
227
|
+
expired(now = Date.now()): ConversationBinding[] {
|
|
228
|
+
const ttl = this.idleTtlMs;
|
|
229
|
+
return this.getBindings().filter((b) => now - b.lastActivity >= ttl);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ==========================================================================
|
|
233
|
+
// Helpers
|
|
234
|
+
// ==========================================================================
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Classify a channel from its descriptor and/or message metadata. Slack
|
|
238
|
+
* marks DMs/group DMs with is_im/is_mpim on the channel descriptor and
|
|
239
|
+
* channel_type on each message; other platforms can adopt either
|
|
240
|
+
* convention.
|
|
241
|
+
*/
|
|
242
|
+
static classifyChannel(
|
|
243
|
+
descriptor?: ChannelDescriptor,
|
|
244
|
+
messageMetadata?: Record<string, unknown>,
|
|
245
|
+
): ChannelKind {
|
|
246
|
+
const meta = descriptor?.metadata as Record<string, unknown> | undefined;
|
|
247
|
+
if (meta?.is_im === true) return 'dm';
|
|
248
|
+
if (meta?.is_mpim === true) return 'groupDm';
|
|
249
|
+
const channelType = messageMetadata?.channel_type;
|
|
250
|
+
if (channelType === 'im') return 'dm';
|
|
251
|
+
if (channelType === 'mpim') return 'groupDm';
|
|
252
|
+
return 'channel';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Export the per-channel engagement counters for persistence — they must
|
|
256
|
+
* survive restarts, or a re-engaged channel reuses generation 1's name and
|
|
257
|
+
* reopens (and re-seeds) the previous engagement's Chronicle namespace. */
|
|
258
|
+
exportGenerations(): Record<string, number> {
|
|
259
|
+
return Object.fromEntries(this.generations);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Restore persisted engagement counters (see exportGenerations). */
|
|
263
|
+
hydrateGenerations(generations: Record<string, number>): void {
|
|
264
|
+
for (const [channelId, generation] of Object.entries(generations)) {
|
|
265
|
+
if (typeof generation === 'number' && generation > (this.generations.get(channelId) ?? 0)) {
|
|
266
|
+
this.generations.set(channelId, generation);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private shouldTrigger(facts: IncomingMessageFacts): boolean {
|
|
272
|
+
const rule = this.config.trigger?.[facts.kind]
|
|
273
|
+
?? (facts.kind === 'dm' ? 'always' : 'mention');
|
|
274
|
+
return rule === 'always' || facts.mentioned;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** `conversation-slack-C123-g2` — agent names double as Chronicle
|
|
278
|
+
* namespace components, so the channel id is sanitized. */
|
|
279
|
+
private forkAgentName(channelId: string, generation: number): string {
|
|
280
|
+
const prefix = this.config.agentPrefix ?? 'conversation';
|
|
281
|
+
const safe = channelId.replace(/[^A-Za-z0-9_-]+/g, '-');
|
|
282
|
+
return `${prefix}-${safe}-g${generation}`;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -145,7 +145,20 @@ export class FeatureSetManager {
|
|
|
145
145
|
capabilities: McplCapabilities,
|
|
146
146
|
config?: { enabledFeatureSets?: string[]; disabledFeatureSets?: string[] }
|
|
147
147
|
): FeatureSetsUpdateParams {
|
|
148
|
-
|
|
148
|
+
// Cross-package compat: agent-framework's McplCapabilities types
|
|
149
|
+
// featureSets as Record<string, FeatureSetDeclaration>, but mcpl-core-ts
|
|
150
|
+
// (used by discord-mcpl and other servers) types it as an array of
|
|
151
|
+
// {name, ...FeatureSetDeclaration}. Both representations arrive over
|
|
152
|
+
// the wire. Normalize to Record here so downstream code (resolvePatterns,
|
|
153
|
+
// validateInbound, etc.) can rely on the keyed shape.
|
|
154
|
+
const featureSetsRaw = capabilities.featureSets ?? {};
|
|
155
|
+
const declared: Record<string, FeatureSetDeclaration> = Array.isArray(featureSetsRaw)
|
|
156
|
+
? Object.fromEntries(
|
|
157
|
+
(featureSetsRaw as ReadonlyArray<FeatureSetDeclaration & { name: string }>).map(
|
|
158
|
+
(d) => [d.name, d],
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
: featureSetsRaw;
|
|
149
162
|
|
|
150
163
|
const state: ServerState = {
|
|
151
164
|
declared: { ...declared },
|
package/src/mcpl/index.ts
CHANGED
|
@@ -130,5 +130,17 @@ export { InferenceRouter } from './inference-router.js';
|
|
|
130
130
|
// Channel registry (channel lifecycle, incoming messages, synthesized tools)
|
|
131
131
|
export { ChannelRegistry } from './channel-registry.js';
|
|
132
132
|
|
|
133
|
+
// Per-channel conversation routing (fork-per-channel agents)
|
|
134
|
+
export { ConversationRouter, DEFAULT_CLOSURE_PROMPT } from './conversation-router.js';
|
|
135
|
+
export type {
|
|
136
|
+
ConversationRouterConfig,
|
|
137
|
+
ConversationBinding,
|
|
138
|
+
RoutingDecision,
|
|
139
|
+
IncomingMessageFacts,
|
|
140
|
+
BindRule,
|
|
141
|
+
TriggerRule,
|
|
142
|
+
ChannelKind,
|
|
143
|
+
} from './conversation-router.js';
|
|
144
|
+
|
|
133
145
|
// State management (Section 8, checkpoint trees + JSON Patch)
|
|
134
146
|
export { CheckpointManager, applyJsonPatch } from './checkpoint-manager.js';
|
package/src/mcpl/push-handler.ts
CHANGED
|
@@ -197,6 +197,7 @@ export class PushHandler {
|
|
|
197
197
|
eventId: params.eventId,
|
|
198
198
|
eventType: 'mcpl:push-event',
|
|
199
199
|
...(params.origin ?? {}),
|
|
200
|
+
...(params.tags ? { tags: params.tags } : {}),
|
|
200
201
|
};
|
|
201
202
|
triggerInference = this.shouldTriggerInference(textContent, metadata);
|
|
202
203
|
}
|