@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,134 @@
|
|
|
1
|
+
// MCPL Protocol Types
|
|
2
|
+
export type {
|
|
3
|
+
// JSON-RPC
|
|
4
|
+
JsonRpcRequest,
|
|
5
|
+
JsonRpcResponse,
|
|
6
|
+
JsonRpcError,
|
|
7
|
+
|
|
8
|
+
// Content blocks (wire format)
|
|
9
|
+
McplContentBlock,
|
|
10
|
+
McplTextContent,
|
|
11
|
+
McplImageContent,
|
|
12
|
+
McplAudioContent,
|
|
13
|
+
McplResourceContent,
|
|
14
|
+
|
|
15
|
+
// Capability negotiation
|
|
16
|
+
McplCapabilities,
|
|
17
|
+
McplHostCapabilities,
|
|
18
|
+
McplChannelCapabilities,
|
|
19
|
+
|
|
20
|
+
// Server configuration
|
|
21
|
+
McplServerConfig,
|
|
22
|
+
|
|
23
|
+
// Feature sets
|
|
24
|
+
FeatureSetUse,
|
|
25
|
+
FeatureSetDeclaration,
|
|
26
|
+
FeatureSetsUpdateParams,
|
|
27
|
+
FeatureSetsChangedParams,
|
|
28
|
+
|
|
29
|
+
// Scoped access
|
|
30
|
+
ScopeConfig,
|
|
31
|
+
ScopeLabel,
|
|
32
|
+
ScopeElevateParams,
|
|
33
|
+
ScopeElevateResult,
|
|
34
|
+
|
|
35
|
+
// State management
|
|
36
|
+
StateCheckpoint,
|
|
37
|
+
JsonPatchOperation,
|
|
38
|
+
StateRollbackParams,
|
|
39
|
+
StateRollbackResult,
|
|
40
|
+
|
|
41
|
+
// Push events
|
|
42
|
+
PushEventParams,
|
|
43
|
+
PushEventResult,
|
|
44
|
+
|
|
45
|
+
// Context hooks
|
|
46
|
+
McplModelInfo,
|
|
47
|
+
BeforeInferenceParams,
|
|
48
|
+
McplContextInjection,
|
|
49
|
+
BeforeInferenceResult,
|
|
50
|
+
AfterInferenceParams,
|
|
51
|
+
AfterInferenceResult,
|
|
52
|
+
|
|
53
|
+
// Server-initiated inference
|
|
54
|
+
McplMessage,
|
|
55
|
+
McplInferenceRequestParams,
|
|
56
|
+
McplInferencePreferences,
|
|
57
|
+
McplInferenceRequestResult,
|
|
58
|
+
InferenceChunkParams,
|
|
59
|
+
|
|
60
|
+
// Channels
|
|
61
|
+
ChannelDescriptor,
|
|
62
|
+
ChannelContext,
|
|
63
|
+
ChannelsRegisterParams,
|
|
64
|
+
ChannelsRegisterResult,
|
|
65
|
+
ChannelsChangedParams,
|
|
66
|
+
ChannelsListResult,
|
|
67
|
+
ChannelsOpenParams,
|
|
68
|
+
ChannelsOpenResult,
|
|
69
|
+
ChannelsCloseParams,
|
|
70
|
+
ChannelsCloseResult,
|
|
71
|
+
ChannelsOutgoingChunkParams,
|
|
72
|
+
ChannelsOutgoingCompleteParams,
|
|
73
|
+
ChannelsPublishParams,
|
|
74
|
+
ChannelsPublishResult,
|
|
75
|
+
ChannelIncomingMessage,
|
|
76
|
+
ChannelsIncomingParams,
|
|
77
|
+
ChannelsIncomingResult,
|
|
78
|
+
ChannelIncomingMessageResult,
|
|
79
|
+
|
|
80
|
+
// Inference routing policy
|
|
81
|
+
InferenceRoutingPolicy,
|
|
82
|
+
|
|
83
|
+
// Method names
|
|
84
|
+
McplMethodName,
|
|
85
|
+
|
|
86
|
+
// MCP standard tool types
|
|
87
|
+
McpToolDefinition,
|
|
88
|
+
McpToolCallResult,
|
|
89
|
+
McpToolResultContent,
|
|
90
|
+
} from './types.js';
|
|
91
|
+
|
|
92
|
+
export { McplMethod } from './types.js';
|
|
93
|
+
|
|
94
|
+
// Error codes and factories
|
|
95
|
+
export {
|
|
96
|
+
FEATURE_SET_NOT_ENABLED,
|
|
97
|
+
UNKNOWN_FEATURE_SET,
|
|
98
|
+
CHECKPOINT_NOT_FOUND,
|
|
99
|
+
CHANNEL_NOT_PERMITTED,
|
|
100
|
+
UNKNOWN_CHANNEL,
|
|
101
|
+
CHANNEL_OPEN_FAILED,
|
|
102
|
+
featureSetNotEnabled,
|
|
103
|
+
unknownFeatureSet,
|
|
104
|
+
checkpointNotFound,
|
|
105
|
+
channelNotPermitted,
|
|
106
|
+
unknownChannel,
|
|
107
|
+
channelOpenFailed,
|
|
108
|
+
} from './errors.js';
|
|
109
|
+
|
|
110
|
+
// Server connection and registry
|
|
111
|
+
export { McplServerConnection } from './server-connection.js';
|
|
112
|
+
export { McplServerRegistry, type McplCapabilityQuery } from './server-registry.js';
|
|
113
|
+
|
|
114
|
+
// Feature set management (permission layer)
|
|
115
|
+
export { FeatureSetManager, McplFeatureSetError } from './feature-set-manager.js';
|
|
116
|
+
|
|
117
|
+
// Scope management (whitelist/blacklist enforcement)
|
|
118
|
+
export { ScopeManager, type ElevationHandler } from './scope-manager.js';
|
|
119
|
+
|
|
120
|
+
// Hook orchestration (beforeInference/afterInference fan-out)
|
|
121
|
+
export { HookOrchestrator } from './hook-orchestrator.js';
|
|
122
|
+
|
|
123
|
+
// Push events (Section 9)
|
|
124
|
+
export { PushHandler } from './push-handler.js';
|
|
125
|
+
export type { McplPushEvent } from './push-handler.js';
|
|
126
|
+
|
|
127
|
+
// Server-initiated inference (Section 11)
|
|
128
|
+
export { InferenceRouter } from './inference-router.js';
|
|
129
|
+
|
|
130
|
+
// Channel registry (channel lifecycle, incoming messages, synthesized tools)
|
|
131
|
+
export { ChannelRegistry } from './channel-registry.js';
|
|
132
|
+
|
|
133
|
+
// State management (Section 8, checkpoint trees + JSON Patch)
|
|
134
|
+
export { CheckpointManager, applyJsonPatch } from './checkpoint-manager.js';
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InferenceRouter — handles inference/request from MCPL servers.
|
|
3
|
+
*
|
|
4
|
+
* Servers can ask the host to run inference on their behalf. This router
|
|
5
|
+
* validates permissions, resolves the target model via routing policy,
|
|
6
|
+
* runs inference through membrane, and optionally streams chunks back.
|
|
7
|
+
*
|
|
8
|
+
* Spec reference: Section 11 (Server-Initiated Inference).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Membrane, ContentBlock, NormalizedRequest } from '@animalabs/membrane';
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
McplInferenceRequestParams,
|
|
15
|
+
McplInferenceRequestResult,
|
|
16
|
+
InferenceChunkParams,
|
|
17
|
+
InferenceRoutingPolicy,
|
|
18
|
+
} from './types.js';
|
|
19
|
+
import type { FeatureSetManager } from './feature-set-manager.js';
|
|
20
|
+
import { McplFeatureSetError } from './feature-set-manager.js';
|
|
21
|
+
import type { HookOrchestrator } from './hook-orchestrator.js';
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Responder interface
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
/** Minimal responder interface for sending JSON-RPC results back. */
|
|
28
|
+
interface Responder {
|
|
29
|
+
respond(result: McplInferenceRequestResult): void;
|
|
30
|
+
respondError(code: number, message: string, data?: unknown): void;
|
|
31
|
+
/** The JSON-RPC request ID, used for streaming chunk correlation. */
|
|
32
|
+
requestId?: string | number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Wildcard matching (same logic as feature-set-manager)
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Simple glob match: split on `.`, `*` matches any single segment.
|
|
41
|
+
*/
|
|
42
|
+
function wildcardMatch(pattern: string, name: string): boolean {
|
|
43
|
+
const patternParts = pattern.split('.');
|
|
44
|
+
const nameParts = name.split('.');
|
|
45
|
+
|
|
46
|
+
if (patternParts.length !== nameParts.length) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
51
|
+
const p = patternParts[i];
|
|
52
|
+
if (p === '*') {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (p !== nameParts[i]) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// Constants
|
|
65
|
+
// ============================================================================
|
|
66
|
+
|
|
67
|
+
/** Default max tokens when not specified in preferences. */
|
|
68
|
+
const DEFAULT_MAX_TOKENS = 4096;
|
|
69
|
+
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// InferenceRouter
|
|
72
|
+
// ============================================================================
|
|
73
|
+
|
|
74
|
+
export class InferenceRouter {
|
|
75
|
+
private membrane: Membrane;
|
|
76
|
+
private hookOrchestrator: HookOrchestrator;
|
|
77
|
+
private featureSetManager: FeatureSetManager;
|
|
78
|
+
private routingPolicy: InferenceRoutingPolicy | null;
|
|
79
|
+
private emitTraceFn: (event: { type: string; [key: string]: unknown }) => void;
|
|
80
|
+
private sendChunkFn?: (serverId: string, params: InferenceChunkParams) => void;
|
|
81
|
+
|
|
82
|
+
constructor(
|
|
83
|
+
membrane: Membrane,
|
|
84
|
+
hookOrchestrator: HookOrchestrator,
|
|
85
|
+
featureSetManager: FeatureSetManager,
|
|
86
|
+
routingPolicy: InferenceRoutingPolicy | null,
|
|
87
|
+
emitTraceFn: (event: { type: string; [key: string]: unknown }) => void,
|
|
88
|
+
sendChunkFn?: (serverId: string, params: InferenceChunkParams) => void,
|
|
89
|
+
) {
|
|
90
|
+
this.membrane = membrane;
|
|
91
|
+
this.hookOrchestrator = hookOrchestrator;
|
|
92
|
+
this.featureSetManager = featureSetManager;
|
|
93
|
+
this.routingPolicy = routingPolicy;
|
|
94
|
+
this.emitTraceFn = emitTraceFn;
|
|
95
|
+
this.sendChunkFn = sendChunkFn;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Handle an inference/request from an MCPL server.
|
|
100
|
+
*
|
|
101
|
+
* 1. Validate feature set
|
|
102
|
+
* 2. Check loop prevention (reject if inside a hook)
|
|
103
|
+
* 3. Resolve model from routing policy
|
|
104
|
+
* 4. Build membrane request and run inference
|
|
105
|
+
* 5. Optionally stream chunks
|
|
106
|
+
* 6. Return result via responder
|
|
107
|
+
*/
|
|
108
|
+
async handleInferenceRequest(
|
|
109
|
+
serverId: string,
|
|
110
|
+
params: McplInferenceRequestParams,
|
|
111
|
+
responder: Responder,
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
// 1. Validate feature set
|
|
114
|
+
try {
|
|
115
|
+
this.featureSetManager.validateInbound(serverId, params.featureSet);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
const message = err instanceof McplFeatureSetError
|
|
118
|
+
? err.message
|
|
119
|
+
: 'Feature set validation failed';
|
|
120
|
+
const code = err instanceof McplFeatureSetError ? err.code : -32600;
|
|
121
|
+
responder.respondError(code, message);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 2. Loop prevention: reject if inside a hook
|
|
126
|
+
if (this.hookOrchestrator.isInHook) {
|
|
127
|
+
responder.respondError(
|
|
128
|
+
-32600,
|
|
129
|
+
'Cannot process inference request during hook execution',
|
|
130
|
+
);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 3. Resolve model
|
|
135
|
+
const model = this.resolveModel(
|
|
136
|
+
params.featureSet,
|
|
137
|
+
params.conversationId,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Emit trace: inference start
|
|
141
|
+
this.emitTraceFn({
|
|
142
|
+
type: 'mcpl:inference_start',
|
|
143
|
+
serverId,
|
|
144
|
+
featureSet: params.featureSet,
|
|
145
|
+
model,
|
|
146
|
+
stream: params.stream ?? false,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// 4. Build membrane-compatible request
|
|
150
|
+
const messages: NormalizedRequest['messages'] = params.messages.map((msg) => ({
|
|
151
|
+
participant: msg.role === 'assistant' ? 'assistant' : 'user',
|
|
152
|
+
content: [{ type: 'text' as const, text: msg.content }],
|
|
153
|
+
}));
|
|
154
|
+
|
|
155
|
+
const request: NormalizedRequest = {
|
|
156
|
+
messages,
|
|
157
|
+
config: {
|
|
158
|
+
model,
|
|
159
|
+
maxTokens: params.preferences?.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
160
|
+
temperature: params.preferences?.temperature,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
// 5. Run inference
|
|
166
|
+
if (params.stream && this.sendChunkFn && responder.requestId != null) {
|
|
167
|
+
// Streaming mode
|
|
168
|
+
await this.handleStreaming(serverId, model, request, responder);
|
|
169
|
+
} else {
|
|
170
|
+
// Non-streaming mode
|
|
171
|
+
const response = await this.membrane.complete(request);
|
|
172
|
+
|
|
173
|
+
// Extract text content from response
|
|
174
|
+
const contentText = response.content
|
|
175
|
+
.filter((b: ContentBlock): b is ContentBlock & { type: 'text' } => b.type === 'text')
|
|
176
|
+
.map((b: ContentBlock & { type: 'text' }) => b.text)
|
|
177
|
+
.join('');
|
|
178
|
+
|
|
179
|
+
const result: McplInferenceRequestResult = {
|
|
180
|
+
content: contentText,
|
|
181
|
+
model,
|
|
182
|
+
finishReason: this.mapStopReason(response.stopReason),
|
|
183
|
+
usage: {
|
|
184
|
+
inputTokens: response.usage.inputTokens,
|
|
185
|
+
outputTokens: response.usage.outputTokens,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Emit trace: inference complete
|
|
190
|
+
this.emitTraceFn({
|
|
191
|
+
type: 'mcpl:inference_complete',
|
|
192
|
+
serverId,
|
|
193
|
+
model,
|
|
194
|
+
inputTokens: result.usage.inputTokens,
|
|
195
|
+
outputTokens: result.usage.outputTokens,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
responder.respond(result);
|
|
199
|
+
}
|
|
200
|
+
} catch (err) {
|
|
201
|
+
// Emit trace: inference failed
|
|
202
|
+
this.emitTraceFn({
|
|
203
|
+
type: 'mcpl:inference_fail',
|
|
204
|
+
serverId,
|
|
205
|
+
model,
|
|
206
|
+
error: err instanceof Error ? err.message : String(err),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
responder.respondError(
|
|
210
|
+
-32603,
|
|
211
|
+
err instanceof Error ? err.message : 'Inference failed',
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ==========================================================================
|
|
217
|
+
// Private: Model resolution
|
|
218
|
+
// ==========================================================================
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Resolve the model to use for an inference request based on routing policy.
|
|
222
|
+
*
|
|
223
|
+
* Priority:
|
|
224
|
+
* 1. Per-conversation override
|
|
225
|
+
* 2. Exact feature set match
|
|
226
|
+
* 3. Wildcard pattern match (first match wins)
|
|
227
|
+
* 4. Default from policy
|
|
228
|
+
* 5. Fallback string 'default'
|
|
229
|
+
*/
|
|
230
|
+
private resolveModel(
|
|
231
|
+
featureSet: string,
|
|
232
|
+
conversationId?: string,
|
|
233
|
+
): string {
|
|
234
|
+
if (!this.routingPolicy) {
|
|
235
|
+
return 'default';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Check per-conversation override
|
|
239
|
+
if (conversationId && this.routingPolicy.overrides?.[conversationId]) {
|
|
240
|
+
return this.routingPolicy.overrides[conversationId];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Check exact feature set match
|
|
244
|
+
if (this.routingPolicy.byFeature?.[featureSet]) {
|
|
245
|
+
return this.routingPolicy.byFeature[featureSet];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Check wildcard patterns
|
|
249
|
+
if (this.routingPolicy.wildcards) {
|
|
250
|
+
for (const [pattern, matchedModel] of Object.entries(this.routingPolicy.wildcards)) {
|
|
251
|
+
if (wildcardMatch(pattern, featureSet)) {
|
|
252
|
+
return matchedModel;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Default
|
|
258
|
+
return this.routingPolicy.default;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ==========================================================================
|
|
262
|
+
// Private: Streaming
|
|
263
|
+
// ==========================================================================
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Handle streaming inference by sending chunks back to the server.
|
|
267
|
+
*
|
|
268
|
+
* Uses membrane.streamYielding when available; falls back to non-streaming
|
|
269
|
+
* membrane.complete otherwise.
|
|
270
|
+
*/
|
|
271
|
+
private async handleStreaming(
|
|
272
|
+
serverId: string,
|
|
273
|
+
model: string,
|
|
274
|
+
request: NormalizedRequest,
|
|
275
|
+
responder: Responder,
|
|
276
|
+
): Promise<void> {
|
|
277
|
+
const requestId = responder.requestId!;
|
|
278
|
+
|
|
279
|
+
// Check if streamYielding is available on the membrane instance.
|
|
280
|
+
// We use streamYielding to iterate events and forward text deltas as chunks.
|
|
281
|
+
const stream = this.membrane.streamYielding(request);
|
|
282
|
+
let chunkIndex = 0;
|
|
283
|
+
let fullContent = '';
|
|
284
|
+
|
|
285
|
+
for await (const event of stream) {
|
|
286
|
+
if (event.type === 'tokens' && 'content' in event) {
|
|
287
|
+
const delta = (event as { type: 'tokens'; content: string }).content;
|
|
288
|
+
fullContent += delta;
|
|
289
|
+
this.sendChunkFn!(serverId, {
|
|
290
|
+
requestId,
|
|
291
|
+
index: chunkIndex++,
|
|
292
|
+
delta,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// After streaming completes, send the final result
|
|
298
|
+
const result: McplInferenceRequestResult = {
|
|
299
|
+
content: fullContent,
|
|
300
|
+
model,
|
|
301
|
+
finishReason: 'end_turn',
|
|
302
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
this.emitTraceFn({
|
|
306
|
+
type: 'mcpl:inference_complete',
|
|
307
|
+
serverId,
|
|
308
|
+
model,
|
|
309
|
+
streamed: true,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
responder.respond(result);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ==========================================================================
|
|
316
|
+
// Private: Helpers
|
|
317
|
+
// ==========================================================================
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Map membrane's stopReason string to MCPL's finishReason enum.
|
|
321
|
+
*/
|
|
322
|
+
private mapStopReason(
|
|
323
|
+
stopReason: string | undefined,
|
|
324
|
+
): 'end_turn' | 'max_tokens' | 'stop_sequence' {
|
|
325
|
+
switch (stopReason) {
|
|
326
|
+
case 'max_tokens':
|
|
327
|
+
return 'max_tokens';
|
|
328
|
+
case 'stop_sequence':
|
|
329
|
+
return 'stop_sequence';
|
|
330
|
+
case 'end_turn':
|
|
331
|
+
default:
|
|
332
|
+
return 'end_turn';
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PushHandler — handles push/event messages from MCPL servers.
|
|
3
|
+
*
|
|
4
|
+
* Validates feature sets, deduplicates by eventId, converts MCPL content blocks
|
|
5
|
+
* to membrane ContentBlock[], and pushes McplPushEvents into the processing queue.
|
|
6
|
+
*
|
|
7
|
+
* Spec reference: Section 9 (Push Events).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ContentBlock } from '@animalabs/membrane';
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
McplContentBlock,
|
|
14
|
+
PushEventParams,
|
|
15
|
+
PushEventResult,
|
|
16
|
+
} from './types.js';
|
|
17
|
+
import type { FeatureSetManager } from './feature-set-manager.js';
|
|
18
|
+
import { McplFeatureSetError } from './feature-set-manager.js';
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// McplPushEvent (the ProcessEvent shape pushed to the queue)
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A push event converted for the framework processing queue.
|
|
26
|
+
*
|
|
27
|
+
* NOTE: This interface should be added to src/types/events.ts and included
|
|
28
|
+
* in the ProcessEvent union. It is defined here for reference but the actual
|
|
29
|
+
* events.ts modification is deferred.
|
|
30
|
+
*/
|
|
31
|
+
export interface McplPushEvent {
|
|
32
|
+
type: 'mcpl:push-event';
|
|
33
|
+
serverId: string;
|
|
34
|
+
featureSet: string;
|
|
35
|
+
eventId: string;
|
|
36
|
+
content: ContentBlock[];
|
|
37
|
+
origin?: Record<string, unknown>;
|
|
38
|
+
timestamp: string;
|
|
39
|
+
inferenceId: string;
|
|
40
|
+
triggerInference?: boolean;
|
|
41
|
+
targetAgents?: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// Content conversion: McplContentBlock → membrane ContentBlock
|
|
46
|
+
// ============================================================================
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Convert a single MCPL wire-format content block to a membrane ContentBlock.
|
|
50
|
+
* Same logic as hook-orchestrator.ts.
|
|
51
|
+
*/
|
|
52
|
+
function convertBlock(block: McplContentBlock): ContentBlock {
|
|
53
|
+
switch (block.type) {
|
|
54
|
+
case 'text':
|
|
55
|
+
return { type: 'text', text: block.text };
|
|
56
|
+
|
|
57
|
+
case 'image':
|
|
58
|
+
if (block.data && block.mimeType) {
|
|
59
|
+
return {
|
|
60
|
+
type: 'image',
|
|
61
|
+
source: { type: 'base64', data: block.data, mediaType: block.mimeType },
|
|
62
|
+
} as ContentBlock;
|
|
63
|
+
}
|
|
64
|
+
if (block.uri) {
|
|
65
|
+
return {
|
|
66
|
+
type: 'image',
|
|
67
|
+
source: { type: 'url', url: block.uri },
|
|
68
|
+
} as ContentBlock;
|
|
69
|
+
}
|
|
70
|
+
return { type: 'text', text: '[Image: no data]' };
|
|
71
|
+
|
|
72
|
+
case 'audio':
|
|
73
|
+
if (block.data && block.mimeType) {
|
|
74
|
+
return {
|
|
75
|
+
type: 'audio',
|
|
76
|
+
source: { type: 'base64', data: block.data, mediaType: block.mimeType },
|
|
77
|
+
} as ContentBlock;
|
|
78
|
+
}
|
|
79
|
+
return { type: 'text', text: '[Audio: no data]' };
|
|
80
|
+
|
|
81
|
+
case 'resource':
|
|
82
|
+
return { type: 'text', text: `[Resource: ${block.uri}]` };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ============================================================================
|
|
87
|
+
// LRU Dedup Set
|
|
88
|
+
// ============================================================================
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Simple dedup set with a max capacity. When full, clears and starts fresh.
|
|
92
|
+
* Good enough for a deduplication window — exact LRU is overkill here.
|
|
93
|
+
*/
|
|
94
|
+
class DedupSet {
|
|
95
|
+
private set = new Set<string>();
|
|
96
|
+
private readonly maxSize: number;
|
|
97
|
+
|
|
98
|
+
constructor(maxSize: number = 1000) {
|
|
99
|
+
this.maxSize = maxSize;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Returns true if the key was already present (duplicate).
|
|
104
|
+
* Otherwise adds it and returns false.
|
|
105
|
+
*/
|
|
106
|
+
checkAndAdd(key: string): boolean {
|
|
107
|
+
if (this.set.has(key)) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (this.set.size >= this.maxSize) {
|
|
111
|
+
this.set.clear();
|
|
112
|
+
}
|
|
113
|
+
this.set.add(key);
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ============================================================================
|
|
119
|
+
// Responder interface
|
|
120
|
+
// ============================================================================
|
|
121
|
+
|
|
122
|
+
/** Minimal responder interface for sending JSON-RPC results back. */
|
|
123
|
+
interface Responder {
|
|
124
|
+
respond(result: PushEventResult): void;
|
|
125
|
+
respondError?(code: number, message: string, data?: unknown): void;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ============================================================================
|
|
129
|
+
// PushHandler
|
|
130
|
+
// ============================================================================
|
|
131
|
+
|
|
132
|
+
export class PushHandler {
|
|
133
|
+
private featureSetManager: FeatureSetManager;
|
|
134
|
+
private pushEventFn: (event: McplPushEvent) => void;
|
|
135
|
+
private emitTraceFn: (event: { type: string; [key: string]: unknown }) => void;
|
|
136
|
+
private shouldTriggerInference?: (content: string, metadata: Record<string, unknown>) => boolean;
|
|
137
|
+
private dedup = new DedupSet(1000);
|
|
138
|
+
|
|
139
|
+
constructor(
|
|
140
|
+
featureSetManager: FeatureSetManager,
|
|
141
|
+
pushEventFn: (event: McplPushEvent) => void,
|
|
142
|
+
emitTraceFn: (event: { type: string; [key: string]: unknown }) => void,
|
|
143
|
+
shouldTriggerInference?: (content: string, metadata: Record<string, unknown>) => boolean,
|
|
144
|
+
) {
|
|
145
|
+
this.featureSetManager = featureSetManager;
|
|
146
|
+
this.pushEventFn = pushEventFn;
|
|
147
|
+
this.emitTraceFn = emitTraceFn;
|
|
148
|
+
this.shouldTriggerInference = shouldTriggerInference;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Handle a push/event message from an MCPL server.
|
|
153
|
+
*
|
|
154
|
+
* 1. Validate feature set
|
|
155
|
+
* 2. Deduplicate by eventId
|
|
156
|
+
* 3. Optionally check shouldTriggerInference callback
|
|
157
|
+
* 4. Convert content blocks
|
|
158
|
+
* 5. Push event to queue
|
|
159
|
+
* 6. Emit trace
|
|
160
|
+
* 7. Respond with accepted + inferenceId
|
|
161
|
+
*/
|
|
162
|
+
handlePushEvent(
|
|
163
|
+
serverId: string,
|
|
164
|
+
params: PushEventParams,
|
|
165
|
+
responder?: Responder,
|
|
166
|
+
): void {
|
|
167
|
+
// 1. Validate feature set
|
|
168
|
+
try {
|
|
169
|
+
this.featureSetManager.validateInbound(serverId, params.featureSet);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
const reason = err instanceof McplFeatureSetError
|
|
172
|
+
? err.message
|
|
173
|
+
: 'Feature set validation failed';
|
|
174
|
+
responder?.respond({ accepted: false, reason });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 2. Deduplicate by eventId
|
|
179
|
+
if (this.dedup.checkAndAdd(params.eventId)) {
|
|
180
|
+
responder?.respond({ accepted: false, reason: 'duplicate' });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 3. Convert content blocks
|
|
185
|
+
const content: ContentBlock[] = params.payload.content.map(convertBlock);
|
|
186
|
+
|
|
187
|
+
// 4. Check shouldTriggerInference callback
|
|
188
|
+
let triggerInference = true;
|
|
189
|
+
if (this.shouldTriggerInference) {
|
|
190
|
+
const textContent = content
|
|
191
|
+
.filter((b): b is ContentBlock & { type: 'text' } => b.type === 'text')
|
|
192
|
+
.map((b) => b.text)
|
|
193
|
+
.join('\n');
|
|
194
|
+
const metadata: Record<string, unknown> = {
|
|
195
|
+
serverId,
|
|
196
|
+
featureSet: params.featureSet,
|
|
197
|
+
eventId: params.eventId,
|
|
198
|
+
eventType: 'push:event',
|
|
199
|
+
...(params.origin ?? {}),
|
|
200
|
+
};
|
|
201
|
+
triggerInference = this.shouldTriggerInference(textContent, metadata);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 5. Generate inferenceId
|
|
205
|
+
const inferenceId = `${serverId}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
206
|
+
|
|
207
|
+
// 6. Push event to queue
|
|
208
|
+
const pushEvent: McplPushEvent = {
|
|
209
|
+
type: 'mcpl:push-event',
|
|
210
|
+
serverId,
|
|
211
|
+
featureSet: params.featureSet,
|
|
212
|
+
eventId: params.eventId,
|
|
213
|
+
content,
|
|
214
|
+
origin: params.origin,
|
|
215
|
+
timestamp: params.timestamp,
|
|
216
|
+
inferenceId,
|
|
217
|
+
triggerInference,
|
|
218
|
+
};
|
|
219
|
+
this.pushEventFn(pushEvent);
|
|
220
|
+
|
|
221
|
+
// 7. Emit trace
|
|
222
|
+
this.emitTraceFn({
|
|
223
|
+
type: 'mcpl:push_event',
|
|
224
|
+
serverId,
|
|
225
|
+
eventId: params.eventId,
|
|
226
|
+
featureSet: params.featureSet,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// 8. Respond
|
|
230
|
+
responder?.respond({ accepted: true, inferenceId });
|
|
231
|
+
}
|
|
232
|
+
}
|