@animalabs/agent-framework 0.1.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/LICENSE +21 -0
- package/README.md +233 -0
- package/dist/src/agent.d.ts +130 -0
- package/dist/src/agent.d.ts.map +1 -0
- package/dist/src/agent.js +406 -0
- package/dist/src/agent.js.map +1 -0
- package/dist/src/api/mcp-server.d.ts +37 -0
- package/dist/src/api/mcp-server.d.ts.map +1 -0
- package/dist/src/api/mcp-server.js +598 -0
- package/dist/src/api/mcp-server.js.map +1 -0
- package/dist/src/api/server.d.ts +83 -0
- package/dist/src/api/server.d.ts.map +1 -0
- package/dist/src/api/server.js +785 -0
- package/dist/src/api/server.js.map +1 -0
- package/dist/src/api/types.d.ts +395 -0
- package/dist/src/api/types.d.ts.map +1 -0
- package/dist/src/api/types.js +5 -0
- package/dist/src/api/types.js.map +1 -0
- package/dist/src/framework.d.ts +309 -0
- package/dist/src/framework.d.ts.map +1 -0
- package/dist/src/framework.js +2309 -0
- package/dist/src/framework.js.map +1 -0
- package/dist/src/gate/event-gate.d.ts +75 -0
- package/dist/src/gate/event-gate.d.ts.map +1 -0
- package/dist/src/gate/event-gate.js +503 -0
- package/dist/src/gate/event-gate.js.map +1 -0
- package/dist/src/gate/index.d.ts +3 -0
- package/dist/src/gate/index.d.ts.map +1 -0
- package/dist/src/gate/index.js +2 -0
- package/dist/src/gate/index.js.map +1 -0
- package/dist/src/gate/types.d.ts +82 -0
- package/dist/src/gate/types.d.ts.map +1 -0
- package/dist/src/gate/types.js +9 -0
- package/dist/src/gate/types.js.map +1 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +18 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/mcpl/channel-registry.d.ts +141 -0
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -0
- package/dist/src/mcpl/channel-registry.js +647 -0
- package/dist/src/mcpl/channel-registry.js.map +1 -0
- package/dist/src/mcpl/checkpoint-manager.d.ts +79 -0
- package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -0
- package/dist/src/mcpl/checkpoint-manager.js +430 -0
- package/dist/src/mcpl/checkpoint-manager.js.map +1 -0
- package/dist/src/mcpl/errors.d.ts +52 -0
- package/dist/src/mcpl/errors.d.ts.map +1 -0
- package/dist/src/mcpl/errors.js +93 -0
- package/dist/src/mcpl/errors.js.map +1 -0
- package/dist/src/mcpl/feature-set-manager.d.ts +91 -0
- package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -0
- package/dist/src/mcpl/feature-set-manager.js +250 -0
- package/dist/src/mcpl/feature-set-manager.js.map +1 -0
- package/dist/src/mcpl/hook-orchestrator.d.ts +51 -0
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -0
- package/dist/src/mcpl/hook-orchestrator.js +217 -0
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -0
- package/dist/src/mcpl/index.d.ts +14 -0
- package/dist/src/mcpl/index.d.ts.map +1 -0
- package/dist/src/mcpl/index.js +21 -0
- package/dist/src/mcpl/index.js.map +1 -0
- package/dist/src/mcpl/inference-router.d.ts +67 -0
- package/dist/src/mcpl/inference-router.d.ts.map +1 -0
- package/dist/src/mcpl/inference-router.js +249 -0
- package/dist/src/mcpl/inference-router.js.map +1 -0
- package/dist/src/mcpl/push-handler.d.ts +60 -0
- package/dist/src/mcpl/push-handler.d.ts.map +1 -0
- package/dist/src/mcpl/push-handler.js +162 -0
- package/dist/src/mcpl/push-handler.js.map +1 -0
- package/dist/src/mcpl/scope-manager.d.ts +76 -0
- package/dist/src/mcpl/scope-manager.d.ts.map +1 -0
- package/dist/src/mcpl/scope-manager.js +213 -0
- package/dist/src/mcpl/scope-manager.js.map +1 -0
- package/dist/src/mcpl/server-connection.d.ts +174 -0
- package/dist/src/mcpl/server-connection.d.ts.map +1 -0
- package/dist/src/mcpl/server-connection.js +559 -0
- package/dist/src/mcpl/server-connection.js.map +1 -0
- package/dist/src/mcpl/server-registry.d.ts +63 -0
- package/dist/src/mcpl/server-registry.d.ts.map +1 -0
- package/dist/src/mcpl/server-registry.js +119 -0
- package/dist/src/mcpl/server-registry.js.map +1 -0
- package/dist/src/mcpl/types.d.ts +755 -0
- package/dist/src/mcpl/types.d.ts.map +1 -0
- package/dist/src/mcpl/types.js +48 -0
- package/dist/src/mcpl/types.js.map +1 -0
- package/dist/src/module-registry.d.ts +93 -0
- package/dist/src/module-registry.d.ts.map +1 -0
- package/dist/src/module-registry.js +386 -0
- package/dist/src/module-registry.js.map +1 -0
- package/dist/src/modules/api/index.d.ts +22 -0
- package/dist/src/modules/api/index.d.ts.map +1 -0
- package/dist/src/modules/api/index.js +63 -0
- package/dist/src/modules/api/index.js.map +1 -0
- package/dist/src/modules/discord/discord-js-client.d.ts +58 -0
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -0
- package/dist/src/modules/discord/discord-js-client.js +273 -0
- package/dist/src/modules/discord/discord-js-client.js.map +1 -0
- package/dist/src/modules/discord/index.d.ts +82 -0
- package/dist/src/modules/discord/index.d.ts.map +1 -0
- package/dist/src/modules/discord/index.js +959 -0
- package/dist/src/modules/discord/index.js.map +1 -0
- package/dist/src/modules/discord/types.d.ts +327 -0
- package/dist/src/modules/discord/types.d.ts.map +1 -0
- package/dist/src/modules/discord/types.js +5 -0
- package/dist/src/modules/discord/types.js.map +1 -0
- package/dist/src/modules/index.d.ts +11 -0
- package/dist/src/modules/index.d.ts.map +1 -0
- package/dist/src/modules/index.js +7 -0
- package/dist/src/modules/index.js.map +1 -0
- package/dist/src/modules/workspace/index.d.ts +59 -0
- package/dist/src/modules/workspace/index.d.ts.map +1 -0
- package/dist/src/modules/workspace/index.js +831 -0
- package/dist/src/modules/workspace/index.js.map +1 -0
- package/dist/src/modules/workspace/sync.d.ts +48 -0
- package/dist/src/modules/workspace/sync.d.ts.map +1 -0
- package/dist/src/modules/workspace/sync.js +253 -0
- package/dist/src/modules/workspace/sync.js.map +1 -0
- package/dist/src/modules/workspace/types.d.ts +166 -0
- package/dist/src/modules/workspace/types.d.ts.map +1 -0
- package/dist/src/modules/workspace/types.js +5 -0
- package/dist/src/modules/workspace/types.js.map +1 -0
- package/dist/src/modules/workspace/watcher.d.ts +43 -0
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -0
- package/dist/src/modules/workspace/watcher.js +120 -0
- package/dist/src/modules/workspace/watcher.js.map +1 -0
- package/dist/src/queue.d.ts +50 -0
- package/dist/src/queue.d.ts.map +1 -0
- package/dist/src/queue.js +95 -0
- package/dist/src/queue.js.map +1 -0
- package/dist/src/safe-slice.d.ts +8 -0
- package/dist/src/safe-slice.d.ts.map +1 -0
- package/dist/src/safe-slice.js +18 -0
- package/dist/src/safe-slice.js.map +1 -0
- package/dist/src/types/agent.d.ts +118 -0
- package/dist/src/types/agent.d.ts.map +1 -0
- package/dist/src/types/agent.js +2 -0
- package/dist/src/types/agent.js.map +1 -0
- package/dist/src/types/events.d.ts +220 -0
- package/dist/src/types/events.d.ts.map +1 -0
- package/dist/src/types/events.js +2 -0
- package/dist/src/types/events.js.map +1 -0
- package/dist/src/types/framework.d.ts +184 -0
- package/dist/src/types/framework.d.ts.map +1 -0
- package/dist/src/types/framework.js +2 -0
- package/dist/src/types/framework.js.map +1 -0
- package/dist/src/types/index.d.ts +7 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +7 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/types/inference-log.d.ts +64 -0
- package/dist/src/types/inference-log.d.ts.map +1 -0
- package/dist/src/types/inference-log.js +5 -0
- package/dist/src/types/inference-log.js.map +1 -0
- package/dist/src/types/module.d.ts +302 -0
- package/dist/src/types/module.d.ts.map +1 -0
- package/dist/src/types/module.js +2 -0
- package/dist/src/types/module.js.map +1 -0
- package/dist/src/types/trace.d.ts +173 -0
- package/dist/src/types/trace.d.ts.map +1 -0
- package/dist/src/types/trace.js +2 -0
- package/dist/src/types/trace.js.map +1 -0
- package/dist/test/event-gate.test.d.ts +2 -0
- package/dist/test/event-gate.test.d.ts.map +1 -0
- package/dist/test/event-gate.test.js +535 -0
- package/dist/test/event-gate.test.js.map +1 -0
- package/dist/test/framework.test.d.ts +5 -0
- package/dist/test/framework.test.d.ts.map +1 -0
- package/dist/test/framework.test.js +738 -0
- package/dist/test/framework.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +58 -0
- package/src/agent.ts +512 -0
- package/src/api/mcp-server.ts +687 -0
- package/src/api/server.ts +1023 -0
- package/src/api/types.ts +542 -0
- package/src/framework.ts +2784 -0
- package/src/gate/event-gate.ts +623 -0
- package/src/gate/index.ts +12 -0
- package/src/gate/types.ts +112 -0
- package/src/index.ts +63 -0
- package/src/mcpl/channel-registry.ts +824 -0
- package/src/mcpl/checkpoint-manager.ts +545 -0
- package/src/mcpl/errors.ts +114 -0
- package/src/mcpl/feature-set-manager.ts +333 -0
- package/src/mcpl/hook-orchestrator.ts +281 -0
- package/src/mcpl/index.ts +134 -0
- package/src/mcpl/inference-router.ts +335 -0
- package/src/mcpl/push-handler.ts +232 -0
- package/src/mcpl/scope-manager.ts +262 -0
- package/src/mcpl/server-connection.ts +694 -0
- package/src/mcpl/server-registry.ts +144 -0
- package/src/mcpl/types.ts +991 -0
- package/src/module-registry.ts +547 -0
- package/src/modules/api/index.ts +94 -0
- package/src/modules/discord/discord-js-client.ts +353 -0
- package/src/modules/discord/index.ts +1146 -0
- package/src/modules/discord/types.ts +465 -0
- package/src/modules/index.ts +43 -0
- package/src/modules/workspace/index.ts +969 -0
- package/src/modules/workspace/sync.ts +314 -0
- package/src/modules/workspace/types.ts +202 -0
- package/src/modules/workspace/watcher.ts +142 -0
- package/src/queue.ts +106 -0
- package/src/safe-slice.ts +18 -0
- package/src/types/agent.ts +119 -0
- package/src/types/events.ts +252 -0
- package/src/types/framework.ts +211 -0
- package/src/types/index.ts +6 -0
- package/src/types/inference-log.ts +64 -0
- package/src/types/module.ts +367 -0
- package/src/types/trace.ts +196 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FeatureSetManager — Permission gate for MCPL feature sets.
|
|
3
|
+
*
|
|
4
|
+
* Manages per-server feature set state: which feature sets each server declares,
|
|
5
|
+
* which are enabled/disabled, and validates inbound messages against that state.
|
|
6
|
+
*
|
|
7
|
+
* Spec references: Section 6 (Feature Sets), Appendix A (Error Codes).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
McplCapabilities,
|
|
12
|
+
FeatureSetDeclaration,
|
|
13
|
+
FeatureSetsUpdateParams,
|
|
14
|
+
FeatureSetsChangedParams,
|
|
15
|
+
} from './types.js';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
FEATURE_SET_NOT_ENABLED,
|
|
19
|
+
UNKNOWN_FEATURE_SET,
|
|
20
|
+
} from './errors.js';
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Error Class
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Error thrown when a feature set validation fails.
|
|
28
|
+
* Carries the JSON-RPC error code and the offending feature set name.
|
|
29
|
+
*/
|
|
30
|
+
export class McplFeatureSetError extends Error {
|
|
31
|
+
constructor(
|
|
32
|
+
public readonly code: number,
|
|
33
|
+
public readonly featureSet: string,
|
|
34
|
+
message: string
|
|
35
|
+
) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = 'McplFeatureSetError';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Internal Types
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
/** Per-server tracking state. */
|
|
46
|
+
interface ServerState {
|
|
47
|
+
/** All declared feature sets (keyed by name). */
|
|
48
|
+
declared: Record<string, FeatureSetDeclaration>;
|
|
49
|
+
/** Set of currently enabled feature set names. */
|
|
50
|
+
enabled: Set<string>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ============================================================================
|
|
54
|
+
// Wildcard Matching
|
|
55
|
+
// ============================================================================
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check whether a wildcard pattern matches a feature set name.
|
|
59
|
+
*
|
|
60
|
+
* Rules:
|
|
61
|
+
* - Split both pattern and name on `.`
|
|
62
|
+
* - `*` in a pattern segment matches exactly one segment (any value)
|
|
63
|
+
* - Literal segments must match exactly
|
|
64
|
+
* - Pattern and name must have the same number of segments
|
|
65
|
+
* (unless the last pattern segment is `*`, which matches one segment)
|
|
66
|
+
*
|
|
67
|
+
* Examples:
|
|
68
|
+
* `memory.*` matches `memory.retrieval` but not `memory.a.b`
|
|
69
|
+
* `memory.retrieval` matches only `memory.retrieval`
|
|
70
|
+
*/
|
|
71
|
+
function wildcardMatch(pattern: string, name: string): boolean {
|
|
72
|
+
const patternParts = pattern.split('.');
|
|
73
|
+
const nameParts = name.split('.');
|
|
74
|
+
|
|
75
|
+
if (patternParts.length !== nameParts.length) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
80
|
+
const p = patternParts[i];
|
|
81
|
+
if (p === '*') {
|
|
82
|
+
// Wildcard segment matches any single segment
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (p !== nameParts[i]) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Find all declared feature set names that match any of the given patterns.
|
|
95
|
+
* Patterns may contain wildcards (e.g., `memory.*`).
|
|
96
|
+
*/
|
|
97
|
+
function resolvePatterns(
|
|
98
|
+
patterns: string[],
|
|
99
|
+
declared: Record<string, FeatureSetDeclaration>
|
|
100
|
+
): string[] {
|
|
101
|
+
const matched = new Set<string>();
|
|
102
|
+
const declaredNames = Object.keys(declared);
|
|
103
|
+
|
|
104
|
+
for (const pattern of patterns) {
|
|
105
|
+
for (const name of declaredNames) {
|
|
106
|
+
if (wildcardMatch(pattern, name)) {
|
|
107
|
+
matched.add(name);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return [...matched];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// FeatureSetManager
|
|
117
|
+
// ============================================================================
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Manages feature set state across all MCPL servers.
|
|
121
|
+
*
|
|
122
|
+
* This is the permission gate that all MCPL interactions pass through.
|
|
123
|
+
* Every inbound server message tagged with a `featureSet` must be validated
|
|
124
|
+
* through this manager before processing.
|
|
125
|
+
*/
|
|
126
|
+
export class FeatureSetManager {
|
|
127
|
+
/** Per-server state, keyed by server ID. */
|
|
128
|
+
private servers = new Map<string, ServerState>();
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Initialize a server's feature set state from its capabilities and host config.
|
|
132
|
+
*
|
|
133
|
+
* Called after a server connects and advertises its capabilities.
|
|
134
|
+
* Returns the `FeatureSetsUpdateParams` that should be sent to the server
|
|
135
|
+
* via `featureSets/update`.
|
|
136
|
+
*
|
|
137
|
+
* Initialization logic:
|
|
138
|
+
* 1. Store all declared feature sets from capabilities.
|
|
139
|
+
* 2. If `config.enabledFeatureSets` is provided, enable matching sets (supports wildcards).
|
|
140
|
+
* 3. If `config.disabledFeatureSets` is provided, disable matching sets (supports wildcards).
|
|
141
|
+
* 4. Feature sets not mentioned in either list default to disabled.
|
|
142
|
+
*/
|
|
143
|
+
initializeServer(
|
|
144
|
+
serverId: string,
|
|
145
|
+
capabilities: McplCapabilities,
|
|
146
|
+
config?: { enabledFeatureSets?: string[]; disabledFeatureSets?: string[] }
|
|
147
|
+
): FeatureSetsUpdateParams {
|
|
148
|
+
const declared = capabilities.featureSets ?? {};
|
|
149
|
+
|
|
150
|
+
const state: ServerState = {
|
|
151
|
+
declared: { ...declared },
|
|
152
|
+
enabled: new Set<string>(),
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Resolve which feature sets to enable
|
|
156
|
+
const enablePatterns = config?.enabledFeatureSets ?? [];
|
|
157
|
+
const disablePatterns = config?.disabledFeatureSets ?? [];
|
|
158
|
+
|
|
159
|
+
const toEnable = resolvePatterns(enablePatterns, declared);
|
|
160
|
+
const toDisable = resolvePatterns(disablePatterns, declared);
|
|
161
|
+
|
|
162
|
+
// Enable matching sets
|
|
163
|
+
for (const name of toEnable) {
|
|
164
|
+
state.enabled.add(name);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Disable explicitly overrides enable (disable wins on conflict)
|
|
168
|
+
for (const name of toDisable) {
|
|
169
|
+
state.enabled.delete(name);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.servers.set(serverId, state);
|
|
173
|
+
|
|
174
|
+
// Build the update params to send to the server
|
|
175
|
+
const enabled = [...state.enabled];
|
|
176
|
+
const allDeclared = Object.keys(declared);
|
|
177
|
+
const disabled = allDeclared.filter((n) => !state.enabled.has(n));
|
|
178
|
+
|
|
179
|
+
const params: FeatureSetsUpdateParams = {};
|
|
180
|
+
if (enabled.length > 0) {
|
|
181
|
+
params.enabled = enabled;
|
|
182
|
+
}
|
|
183
|
+
if (disabled.length > 0) {
|
|
184
|
+
params.disabled = disabled;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return params;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Remove all tracking state for a server.
|
|
192
|
+
* Called when a server disconnects.
|
|
193
|
+
*/
|
|
194
|
+
removeServer(serverId: string): void {
|
|
195
|
+
this.servers.delete(serverId);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Check whether a feature set is enabled for a server.
|
|
200
|
+
* Returns false if the server is unknown or the feature set is not enabled.
|
|
201
|
+
*/
|
|
202
|
+
isEnabled(serverId: string, featureSet: string): boolean {
|
|
203
|
+
const state = this.servers.get(serverId);
|
|
204
|
+
if (!state) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
return state.enabled.has(featureSet);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Validate that an inbound message's feature set is declared and enabled.
|
|
212
|
+
*
|
|
213
|
+
* @throws McplFeatureSetError with code -32003 if the feature set is unknown
|
|
214
|
+
* @throws McplFeatureSetError with code -32001 if the feature set is not enabled
|
|
215
|
+
*/
|
|
216
|
+
validateInbound(serverId: string, featureSet: string): void {
|
|
217
|
+
const state = this.servers.get(serverId);
|
|
218
|
+
|
|
219
|
+
if (!state) {
|
|
220
|
+
throw new McplFeatureSetError(
|
|
221
|
+
UNKNOWN_FEATURE_SET,
|
|
222
|
+
featureSet,
|
|
223
|
+
`Unknown server: ${serverId}`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!(featureSet in state.declared)) {
|
|
228
|
+
throw new McplFeatureSetError(
|
|
229
|
+
UNKNOWN_FEATURE_SET,
|
|
230
|
+
featureSet,
|
|
231
|
+
`Unknown feature set: ${featureSet}`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!state.enabled.has(featureSet)) {
|
|
236
|
+
throw new McplFeatureSetError(
|
|
237
|
+
FEATURE_SET_NOT_ENABLED,
|
|
238
|
+
featureSet,
|
|
239
|
+
`Feature set not enabled: ${featureSet}`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Enable feature sets for a server (supports wildcard patterns).
|
|
246
|
+
* Only declared feature sets can be enabled.
|
|
247
|
+
*/
|
|
248
|
+
enable(serverId: string, featureSets: string[]): void {
|
|
249
|
+
const state = this.servers.get(serverId);
|
|
250
|
+
if (!state) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const resolved = resolvePatterns(featureSets, state.declared);
|
|
255
|
+
for (const name of resolved) {
|
|
256
|
+
state.enabled.add(name);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Disable feature sets for a server (supports wildcard patterns).
|
|
262
|
+
* Only declared feature sets can be disabled.
|
|
263
|
+
*/
|
|
264
|
+
disable(serverId: string, featureSets: string[]): void {
|
|
265
|
+
const state = this.servers.get(serverId);
|
|
266
|
+
if (!state) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const resolved = resolvePatterns(featureSets, state.declared);
|
|
271
|
+
for (const name of resolved) {
|
|
272
|
+
state.enabled.delete(name);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Handle a `featureSets/changed` notification from a server.
|
|
278
|
+
*
|
|
279
|
+
* - Adds new feature sets from `params.added` (default to disabled).
|
|
280
|
+
* - Removes feature sets listed in `params.removed`.
|
|
281
|
+
*/
|
|
282
|
+
handleFeatureSetsChanged(
|
|
283
|
+
serverId: string,
|
|
284
|
+
params: FeatureSetsChangedParams
|
|
285
|
+
): void {
|
|
286
|
+
const state = this.servers.get(serverId);
|
|
287
|
+
if (!state) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Add new feature sets (default to disabled)
|
|
292
|
+
if (params.added) {
|
|
293
|
+
for (const [name, declaration] of Object.entries(params.added)) {
|
|
294
|
+
state.declared[name] = declaration;
|
|
295
|
+
// New feature sets default to disabled — do not add to enabled set
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Remove feature sets
|
|
300
|
+
if (params.removed) {
|
|
301
|
+
for (const name of params.removed) {
|
|
302
|
+
delete state.declared[name];
|
|
303
|
+
state.enabled.delete(name);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Get all declared feature sets for a server.
|
|
310
|
+
* Returns null if the server is unknown.
|
|
311
|
+
*/
|
|
312
|
+
getDeclaredFeatureSets(
|
|
313
|
+
serverId: string
|
|
314
|
+
): Record<string, FeatureSetDeclaration> | null {
|
|
315
|
+
const state = this.servers.get(serverId);
|
|
316
|
+
if (!state) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
return { ...state.declared };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Get the names of all enabled feature sets for a server.
|
|
324
|
+
* Returns an empty array if the server is unknown.
|
|
325
|
+
*/
|
|
326
|
+
getEnabledFeatureSets(serverId: string): string[] {
|
|
327
|
+
const state = this.servers.get(serverId);
|
|
328
|
+
if (!state) {
|
|
329
|
+
return [];
|
|
330
|
+
}
|
|
331
|
+
return [...state.enabled];
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HookOrchestrator — manages before/after inference fan-out to MCPL servers.
|
|
3
|
+
*
|
|
4
|
+
* This is the bridge between the MCPL protocol and the agent-framework inference
|
|
5
|
+
* pipeline. It collects ContextInjection[] from MCPL servers via beforeInference
|
|
6
|
+
* and feeds them into context-manager's compile(). After inference, it notifies
|
|
7
|
+
* servers of the result via afterInference.
|
|
8
|
+
*
|
|
9
|
+
* Design principles:
|
|
10
|
+
* - Fail-open: timeouts and errors never block inference
|
|
11
|
+
* - Parallel fan-out with per-server timeouts
|
|
12
|
+
* - Loop prevention: rejects inference/request while inside a hook
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ContentBlock } from '@animalabs/membrane';
|
|
16
|
+
import type { ContextInjection } from '@animalabs/context-manager';
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
McplContentBlock,
|
|
20
|
+
McplContextInjection,
|
|
21
|
+
BeforeInferenceParams,
|
|
22
|
+
BeforeInferenceResult,
|
|
23
|
+
AfterInferenceParams,
|
|
24
|
+
AfterInferenceResult,
|
|
25
|
+
} from './types.js';
|
|
26
|
+
import type { McplServerRegistry } from './server-registry.js';
|
|
27
|
+
import type { McplServerConnection } from './server-connection.js';
|
|
28
|
+
import type { FeatureSetManager } from './feature-set-manager.js';
|
|
29
|
+
|
|
30
|
+
/** Timeout for beforeInference per server (fail-open). */
|
|
31
|
+
const BEFORE_INFERENCE_TIMEOUT_MS = 5_000;
|
|
32
|
+
|
|
33
|
+
/** Timeout for blocking afterInference per server. */
|
|
34
|
+
const AFTER_INFERENCE_BLOCKING_TIMEOUT_MS = 10_000;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Races a promise against a timeout. Rejects with a descriptive error on timeout.
|
|
38
|
+
*/
|
|
39
|
+
function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
|
|
40
|
+
return Promise.race([
|
|
41
|
+
promise,
|
|
42
|
+
new Promise<never>((_resolve, reject) =>
|
|
43
|
+
setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms),
|
|
44
|
+
),
|
|
45
|
+
]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ============================================================================
|
|
49
|
+
// Content conversion: McplContentBlock / McplContextInjection → membrane types
|
|
50
|
+
// ============================================================================
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Convert a single MCPL wire-format content block to a membrane ContentBlock.
|
|
54
|
+
*/
|
|
55
|
+
function convertBlock(block: McplContentBlock): ContentBlock {
|
|
56
|
+
switch (block.type) {
|
|
57
|
+
case 'text':
|
|
58
|
+
return { type: 'text', text: block.text };
|
|
59
|
+
|
|
60
|
+
case 'image':
|
|
61
|
+
if (block.data && block.mimeType) {
|
|
62
|
+
return {
|
|
63
|
+
type: 'image',
|
|
64
|
+
source: { type: 'base64', data: block.data, mediaType: block.mimeType },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (block.uri) {
|
|
68
|
+
return {
|
|
69
|
+
type: 'image',
|
|
70
|
+
source: { type: 'url', url: block.uri },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Malformed image block — degrade to text
|
|
74
|
+
return { type: 'text', text: '[Image: missing data]' };
|
|
75
|
+
|
|
76
|
+
case 'audio':
|
|
77
|
+
if (block.data && block.mimeType) {
|
|
78
|
+
return {
|
|
79
|
+
type: 'audio',
|
|
80
|
+
source: { type: 'base64', data: block.data, mediaType: block.mimeType },
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// Audio without inline data — degrade to text
|
|
84
|
+
return { type: 'text', text: `[Audio: ${block.uri ?? 'missing data'}]` };
|
|
85
|
+
|
|
86
|
+
case 'resource':
|
|
87
|
+
// Resources don't have a direct membrane equivalent — degrade to text
|
|
88
|
+
return { type: 'text', text: `[Resource: ${block.uri}]` };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Convert MCPL injection content (string shorthand or block array) to membrane ContentBlock[].
|
|
94
|
+
*/
|
|
95
|
+
function convertContent(content: string | McplContentBlock[]): ContentBlock[] {
|
|
96
|
+
if (typeof content === 'string') {
|
|
97
|
+
return [{ type: 'text', text: content }];
|
|
98
|
+
}
|
|
99
|
+
return content.map(convertBlock);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Convert an MCPL wire-format context injection to a context-manager ContextInjection.
|
|
104
|
+
*/
|
|
105
|
+
function convertMcplInjection(mcplInj: McplContextInjection): ContextInjection {
|
|
106
|
+
return {
|
|
107
|
+
namespace: mcplInj.namespace,
|
|
108
|
+
position: mcplInj.position,
|
|
109
|
+
content: convertContent(mcplInj.content),
|
|
110
|
+
metadata: mcplInj.metadata,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ============================================================================
|
|
115
|
+
// HookOrchestrator
|
|
116
|
+
// ============================================================================
|
|
117
|
+
|
|
118
|
+
export class HookOrchestrator {
|
|
119
|
+
private registry: McplServerRegistry;
|
|
120
|
+
private featureSetManager: FeatureSetManager;
|
|
121
|
+
private _isInHook = false;
|
|
122
|
+
|
|
123
|
+
constructor(registry: McplServerRegistry, featureSetManager: FeatureSetManager) {
|
|
124
|
+
this.registry = registry;
|
|
125
|
+
this.featureSetManager = featureSetManager;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Whether a hook is currently executing.
|
|
130
|
+
* Used for loop prevention: inference/request from servers should be rejected
|
|
131
|
+
* while this is true (enforced by Step 6's InferenceRouter).
|
|
132
|
+
*/
|
|
133
|
+
get isInHook(): boolean {
|
|
134
|
+
return this._isInHook;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Fan out `context/beforeInference` to all capable MCPL servers in parallel.
|
|
139
|
+
*
|
|
140
|
+
* Returns aggregated ContextInjection[] ready for context-manager's compile().
|
|
141
|
+
* Fail-open: servers that time out or error are silently skipped.
|
|
142
|
+
*/
|
|
143
|
+
async beforeInference(params: BeforeInferenceParams): Promise<ContextInjection[]> {
|
|
144
|
+
const servers = this.registry.getServersWithCapability('contextHooks.beforeInference');
|
|
145
|
+
if (servers.length === 0) {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this._isInHook = true;
|
|
150
|
+
try {
|
|
151
|
+
return await this.fanOutBeforeInference(servers, params);
|
|
152
|
+
} finally {
|
|
153
|
+
this._isInHook = false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Fan out `context/afterInference` to all capable MCPL servers.
|
|
159
|
+
*
|
|
160
|
+
* Non-blocking servers receive a notification (fire-and-forget).
|
|
161
|
+
* Blocking servers receive a request with a 10s timeout.
|
|
162
|
+
* Returns the first valid AfterInferenceResult from a blocking server, or null.
|
|
163
|
+
*/
|
|
164
|
+
async afterInference(params: AfterInferenceParams): Promise<AfterInferenceResult | null> {
|
|
165
|
+
const servers = this.registry.getServersWithCapability('contextHooks.afterInference');
|
|
166
|
+
if (servers.length === 0) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this._isInHook = true;
|
|
171
|
+
try {
|
|
172
|
+
return await this.fanOutAfterInference(servers, params);
|
|
173
|
+
} finally {
|
|
174
|
+
this._isInHook = false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ==========================================================================
|
|
179
|
+
// Private: beforeInference fan-out
|
|
180
|
+
// ==========================================================================
|
|
181
|
+
|
|
182
|
+
private async fanOutBeforeInference(
|
|
183
|
+
servers: McplServerConnection[],
|
|
184
|
+
params: BeforeInferenceParams,
|
|
185
|
+
): Promise<ContextInjection[]> {
|
|
186
|
+
const results = await Promise.allSettled(
|
|
187
|
+
servers.map((server) =>
|
|
188
|
+
withTimeout(
|
|
189
|
+
server.sendBeforeInference(params),
|
|
190
|
+
BEFORE_INFERENCE_TIMEOUT_MS,
|
|
191
|
+
`beforeInference to "${server.id}"`,
|
|
192
|
+
).then((result) => ({ server, result })),
|
|
193
|
+
),
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const injections: ContextInjection[] = [];
|
|
197
|
+
|
|
198
|
+
for (const settled of results) {
|
|
199
|
+
if (settled.status === 'rejected') {
|
|
200
|
+
// Fail-open: timeout or transport error — skip this server
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const { server, result } = settled.value;
|
|
205
|
+
|
|
206
|
+
// Validate feature set
|
|
207
|
+
try {
|
|
208
|
+
this.featureSetManager.validateInbound(server.id, result.featureSet);
|
|
209
|
+
} catch {
|
|
210
|
+
// Feature set not enabled or unknown — skip injections from this server
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Convert wire-format injections to context-manager format
|
|
215
|
+
if (result.contextInjections && result.contextInjections.length > 0) {
|
|
216
|
+
for (const mcplInj of result.contextInjections) {
|
|
217
|
+
injections.push(convertMcplInjection(mcplInj));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return injections;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ==========================================================================
|
|
226
|
+
// Private: afterInference fan-out
|
|
227
|
+
// ==========================================================================
|
|
228
|
+
|
|
229
|
+
private async fanOutAfterInference(
|
|
230
|
+
servers: McplServerConnection[],
|
|
231
|
+
params: AfterInferenceParams,
|
|
232
|
+
): Promise<AfterInferenceResult | null> {
|
|
233
|
+
const blockingPromises: Promise<AfterInferenceResult | null>[] = [];
|
|
234
|
+
|
|
235
|
+
for (const server of servers) {
|
|
236
|
+
const isBlocking = this.isBlockingAfterInference(server);
|
|
237
|
+
|
|
238
|
+
if (isBlocking) {
|
|
239
|
+
// Send as request, await response with timeout
|
|
240
|
+
const promise = withTimeout(
|
|
241
|
+
server.sendAfterInference(params, true) as Promise<AfterInferenceResult>,
|
|
242
|
+
AFTER_INFERENCE_BLOCKING_TIMEOUT_MS,
|
|
243
|
+
`afterInference (blocking) to "${server.id}"`,
|
|
244
|
+
).catch((): null => {
|
|
245
|
+
// Fail-open: timeout or error — treat as no modification
|
|
246
|
+
return null;
|
|
247
|
+
});
|
|
248
|
+
blockingPromises.push(promise);
|
|
249
|
+
} else {
|
|
250
|
+
// Send as notification (fire-and-forget)
|
|
251
|
+
server.sendAfterInference(params, false);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (blockingPromises.length === 0) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Await all blocking responses and return the first valid result
|
|
260
|
+
const results = await Promise.all(blockingPromises);
|
|
261
|
+
for (const result of results) {
|
|
262
|
+
if (result && result.modifiedResponse) {
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Check if a server's afterInference capability is blocking.
|
|
272
|
+
*/
|
|
273
|
+
private isBlockingAfterInference(server: McplServerConnection): boolean {
|
|
274
|
+
const caps = server.capabilities;
|
|
275
|
+
if (!caps?.contextHooks?.afterInference) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
const afterCap = caps.contextHooks.afterInference;
|
|
279
|
+
return typeof afterCap === 'object' && afterCap !== null && 'blocking' in afterCap && afterCap.blocking === true;
|
|
280
|
+
}
|
|
281
|
+
}
|