@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.
Files changed (212) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -0
  3. package/dist/src/agent.d.ts +130 -0
  4. package/dist/src/agent.d.ts.map +1 -0
  5. package/dist/src/agent.js +406 -0
  6. package/dist/src/agent.js.map +1 -0
  7. package/dist/src/api/mcp-server.d.ts +37 -0
  8. package/dist/src/api/mcp-server.d.ts.map +1 -0
  9. package/dist/src/api/mcp-server.js +598 -0
  10. package/dist/src/api/mcp-server.js.map +1 -0
  11. package/dist/src/api/server.d.ts +83 -0
  12. package/dist/src/api/server.d.ts.map +1 -0
  13. package/dist/src/api/server.js +785 -0
  14. package/dist/src/api/server.js.map +1 -0
  15. package/dist/src/api/types.d.ts +395 -0
  16. package/dist/src/api/types.d.ts.map +1 -0
  17. package/dist/src/api/types.js +5 -0
  18. package/dist/src/api/types.js.map +1 -0
  19. package/dist/src/framework.d.ts +309 -0
  20. package/dist/src/framework.d.ts.map +1 -0
  21. package/dist/src/framework.js +2309 -0
  22. package/dist/src/framework.js.map +1 -0
  23. package/dist/src/gate/event-gate.d.ts +75 -0
  24. package/dist/src/gate/event-gate.d.ts.map +1 -0
  25. package/dist/src/gate/event-gate.js +503 -0
  26. package/dist/src/gate/event-gate.js.map +1 -0
  27. package/dist/src/gate/index.d.ts +3 -0
  28. package/dist/src/gate/index.d.ts.map +1 -0
  29. package/dist/src/gate/index.js +2 -0
  30. package/dist/src/gate/index.js.map +1 -0
  31. package/dist/src/gate/types.d.ts +82 -0
  32. package/dist/src/gate/types.d.ts.map +1 -0
  33. package/dist/src/gate/types.js +9 -0
  34. package/dist/src/gate/types.js.map +1 -0
  35. package/dist/src/index.d.ts +17 -0
  36. package/dist/src/index.d.ts.map +1 -0
  37. package/dist/src/index.js +18 -0
  38. package/dist/src/index.js.map +1 -0
  39. package/dist/src/mcpl/channel-registry.d.ts +141 -0
  40. package/dist/src/mcpl/channel-registry.d.ts.map +1 -0
  41. package/dist/src/mcpl/channel-registry.js +647 -0
  42. package/dist/src/mcpl/channel-registry.js.map +1 -0
  43. package/dist/src/mcpl/checkpoint-manager.d.ts +79 -0
  44. package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -0
  45. package/dist/src/mcpl/checkpoint-manager.js +430 -0
  46. package/dist/src/mcpl/checkpoint-manager.js.map +1 -0
  47. package/dist/src/mcpl/errors.d.ts +52 -0
  48. package/dist/src/mcpl/errors.d.ts.map +1 -0
  49. package/dist/src/mcpl/errors.js +93 -0
  50. package/dist/src/mcpl/errors.js.map +1 -0
  51. package/dist/src/mcpl/feature-set-manager.d.ts +91 -0
  52. package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -0
  53. package/dist/src/mcpl/feature-set-manager.js +250 -0
  54. package/dist/src/mcpl/feature-set-manager.js.map +1 -0
  55. package/dist/src/mcpl/hook-orchestrator.d.ts +51 -0
  56. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -0
  57. package/dist/src/mcpl/hook-orchestrator.js +217 -0
  58. package/dist/src/mcpl/hook-orchestrator.js.map +1 -0
  59. package/dist/src/mcpl/index.d.ts +14 -0
  60. package/dist/src/mcpl/index.d.ts.map +1 -0
  61. package/dist/src/mcpl/index.js +21 -0
  62. package/dist/src/mcpl/index.js.map +1 -0
  63. package/dist/src/mcpl/inference-router.d.ts +67 -0
  64. package/dist/src/mcpl/inference-router.d.ts.map +1 -0
  65. package/dist/src/mcpl/inference-router.js +249 -0
  66. package/dist/src/mcpl/inference-router.js.map +1 -0
  67. package/dist/src/mcpl/push-handler.d.ts +60 -0
  68. package/dist/src/mcpl/push-handler.d.ts.map +1 -0
  69. package/dist/src/mcpl/push-handler.js +162 -0
  70. package/dist/src/mcpl/push-handler.js.map +1 -0
  71. package/dist/src/mcpl/scope-manager.d.ts +76 -0
  72. package/dist/src/mcpl/scope-manager.d.ts.map +1 -0
  73. package/dist/src/mcpl/scope-manager.js +213 -0
  74. package/dist/src/mcpl/scope-manager.js.map +1 -0
  75. package/dist/src/mcpl/server-connection.d.ts +174 -0
  76. package/dist/src/mcpl/server-connection.d.ts.map +1 -0
  77. package/dist/src/mcpl/server-connection.js +559 -0
  78. package/dist/src/mcpl/server-connection.js.map +1 -0
  79. package/dist/src/mcpl/server-registry.d.ts +63 -0
  80. package/dist/src/mcpl/server-registry.d.ts.map +1 -0
  81. package/dist/src/mcpl/server-registry.js +119 -0
  82. package/dist/src/mcpl/server-registry.js.map +1 -0
  83. package/dist/src/mcpl/types.d.ts +755 -0
  84. package/dist/src/mcpl/types.d.ts.map +1 -0
  85. package/dist/src/mcpl/types.js +48 -0
  86. package/dist/src/mcpl/types.js.map +1 -0
  87. package/dist/src/module-registry.d.ts +93 -0
  88. package/dist/src/module-registry.d.ts.map +1 -0
  89. package/dist/src/module-registry.js +386 -0
  90. package/dist/src/module-registry.js.map +1 -0
  91. package/dist/src/modules/api/index.d.ts +22 -0
  92. package/dist/src/modules/api/index.d.ts.map +1 -0
  93. package/dist/src/modules/api/index.js +63 -0
  94. package/dist/src/modules/api/index.js.map +1 -0
  95. package/dist/src/modules/discord/discord-js-client.d.ts +58 -0
  96. package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -0
  97. package/dist/src/modules/discord/discord-js-client.js +273 -0
  98. package/dist/src/modules/discord/discord-js-client.js.map +1 -0
  99. package/dist/src/modules/discord/index.d.ts +82 -0
  100. package/dist/src/modules/discord/index.d.ts.map +1 -0
  101. package/dist/src/modules/discord/index.js +959 -0
  102. package/dist/src/modules/discord/index.js.map +1 -0
  103. package/dist/src/modules/discord/types.d.ts +327 -0
  104. package/dist/src/modules/discord/types.d.ts.map +1 -0
  105. package/dist/src/modules/discord/types.js +5 -0
  106. package/dist/src/modules/discord/types.js.map +1 -0
  107. package/dist/src/modules/index.d.ts +11 -0
  108. package/dist/src/modules/index.d.ts.map +1 -0
  109. package/dist/src/modules/index.js +7 -0
  110. package/dist/src/modules/index.js.map +1 -0
  111. package/dist/src/modules/workspace/index.d.ts +59 -0
  112. package/dist/src/modules/workspace/index.d.ts.map +1 -0
  113. package/dist/src/modules/workspace/index.js +831 -0
  114. package/dist/src/modules/workspace/index.js.map +1 -0
  115. package/dist/src/modules/workspace/sync.d.ts +48 -0
  116. package/dist/src/modules/workspace/sync.d.ts.map +1 -0
  117. package/dist/src/modules/workspace/sync.js +253 -0
  118. package/dist/src/modules/workspace/sync.js.map +1 -0
  119. package/dist/src/modules/workspace/types.d.ts +166 -0
  120. package/dist/src/modules/workspace/types.d.ts.map +1 -0
  121. package/dist/src/modules/workspace/types.js +5 -0
  122. package/dist/src/modules/workspace/types.js.map +1 -0
  123. package/dist/src/modules/workspace/watcher.d.ts +43 -0
  124. package/dist/src/modules/workspace/watcher.d.ts.map +1 -0
  125. package/dist/src/modules/workspace/watcher.js +120 -0
  126. package/dist/src/modules/workspace/watcher.js.map +1 -0
  127. package/dist/src/queue.d.ts +50 -0
  128. package/dist/src/queue.d.ts.map +1 -0
  129. package/dist/src/queue.js +95 -0
  130. package/dist/src/queue.js.map +1 -0
  131. package/dist/src/safe-slice.d.ts +8 -0
  132. package/dist/src/safe-slice.d.ts.map +1 -0
  133. package/dist/src/safe-slice.js +18 -0
  134. package/dist/src/safe-slice.js.map +1 -0
  135. package/dist/src/types/agent.d.ts +118 -0
  136. package/dist/src/types/agent.d.ts.map +1 -0
  137. package/dist/src/types/agent.js +2 -0
  138. package/dist/src/types/agent.js.map +1 -0
  139. package/dist/src/types/events.d.ts +220 -0
  140. package/dist/src/types/events.d.ts.map +1 -0
  141. package/dist/src/types/events.js +2 -0
  142. package/dist/src/types/events.js.map +1 -0
  143. package/dist/src/types/framework.d.ts +184 -0
  144. package/dist/src/types/framework.d.ts.map +1 -0
  145. package/dist/src/types/framework.js +2 -0
  146. package/dist/src/types/framework.js.map +1 -0
  147. package/dist/src/types/index.d.ts +7 -0
  148. package/dist/src/types/index.d.ts.map +1 -0
  149. package/dist/src/types/index.js +7 -0
  150. package/dist/src/types/index.js.map +1 -0
  151. package/dist/src/types/inference-log.d.ts +64 -0
  152. package/dist/src/types/inference-log.d.ts.map +1 -0
  153. package/dist/src/types/inference-log.js +5 -0
  154. package/dist/src/types/inference-log.js.map +1 -0
  155. package/dist/src/types/module.d.ts +302 -0
  156. package/dist/src/types/module.d.ts.map +1 -0
  157. package/dist/src/types/module.js +2 -0
  158. package/dist/src/types/module.js.map +1 -0
  159. package/dist/src/types/trace.d.ts +173 -0
  160. package/dist/src/types/trace.d.ts.map +1 -0
  161. package/dist/src/types/trace.js +2 -0
  162. package/dist/src/types/trace.js.map +1 -0
  163. package/dist/test/event-gate.test.d.ts +2 -0
  164. package/dist/test/event-gate.test.d.ts.map +1 -0
  165. package/dist/test/event-gate.test.js +535 -0
  166. package/dist/test/event-gate.test.js.map +1 -0
  167. package/dist/test/framework.test.d.ts +5 -0
  168. package/dist/test/framework.test.d.ts.map +1 -0
  169. package/dist/test/framework.test.js +738 -0
  170. package/dist/test/framework.test.js.map +1 -0
  171. package/dist/tsconfig.tsbuildinfo +1 -0
  172. package/package.json +58 -0
  173. package/src/agent.ts +512 -0
  174. package/src/api/mcp-server.ts +687 -0
  175. package/src/api/server.ts +1023 -0
  176. package/src/api/types.ts +542 -0
  177. package/src/framework.ts +2784 -0
  178. package/src/gate/event-gate.ts +623 -0
  179. package/src/gate/index.ts +12 -0
  180. package/src/gate/types.ts +112 -0
  181. package/src/index.ts +63 -0
  182. package/src/mcpl/channel-registry.ts +824 -0
  183. package/src/mcpl/checkpoint-manager.ts +545 -0
  184. package/src/mcpl/errors.ts +114 -0
  185. package/src/mcpl/feature-set-manager.ts +333 -0
  186. package/src/mcpl/hook-orchestrator.ts +281 -0
  187. package/src/mcpl/index.ts +134 -0
  188. package/src/mcpl/inference-router.ts +335 -0
  189. package/src/mcpl/push-handler.ts +232 -0
  190. package/src/mcpl/scope-manager.ts +262 -0
  191. package/src/mcpl/server-connection.ts +694 -0
  192. package/src/mcpl/server-registry.ts +144 -0
  193. package/src/mcpl/types.ts +991 -0
  194. package/src/module-registry.ts +547 -0
  195. package/src/modules/api/index.ts +94 -0
  196. package/src/modules/discord/discord-js-client.ts +353 -0
  197. package/src/modules/discord/index.ts +1146 -0
  198. package/src/modules/discord/types.ts +465 -0
  199. package/src/modules/index.ts +43 -0
  200. package/src/modules/workspace/index.ts +969 -0
  201. package/src/modules/workspace/sync.ts +314 -0
  202. package/src/modules/workspace/types.ts +202 -0
  203. package/src/modules/workspace/watcher.ts +142 -0
  204. package/src/queue.ts +106 -0
  205. package/src/safe-slice.ts +18 -0
  206. package/src/types/agent.ts +119 -0
  207. package/src/types/events.ts +252 -0
  208. package/src/types/framework.ts +211 -0
  209. package/src/types/index.ts +6 -0
  210. package/src/types/inference-log.ts +64 -0
  211. package/src/types/module.ts +367 -0
  212. 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
+ }