@animalabs/agent-framework 0.19.0 → 0.21.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 (214) hide show
  1. package/README.md +29 -0
  2. package/dist/src/agent.d.ts +22 -0
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +128 -10
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/types.d.ts +1 -1
  7. package/dist/src/api/types.d.ts.map +1 -1
  8. package/dist/src/code-execution/py-runner.d.ts +10 -1
  9. package/dist/src/code-execution/py-runner.d.ts.map +1 -1
  10. package/dist/src/code-execution/py-runner.js +27 -2
  11. package/dist/src/code-execution/py-runner.js.map +1 -1
  12. package/dist/src/code-execution/tool-definition.d.ts +21 -0
  13. package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
  14. package/dist/src/code-execution/tool-definition.js +34 -0
  15. package/dist/src/code-execution/tool-definition.js.map +1 -1
  16. package/dist/src/framework.d.ts +153 -2
  17. package/dist/src/framework.d.ts.map +1 -1
  18. package/dist/src/framework.js +1175 -76
  19. package/dist/src/framework.js.map +1 -1
  20. package/dist/src/index.d.ts +2 -0
  21. package/dist/src/index.d.ts.map +1 -1
  22. package/dist/src/index.js +4 -0
  23. package/dist/src/index.js.map +1 -1
  24. package/dist/src/mcpl/capability-grant.d.ts +1 -1
  25. package/dist/src/mcpl/capability-grant.d.ts.map +1 -1
  26. package/dist/src/mcpl/capability-grant.js +49 -5
  27. package/dist/src/mcpl/capability-grant.js.map +1 -1
  28. package/dist/src/mcpl/channel-registry.d.ts +59 -2
  29. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  30. package/dist/src/mcpl/channel-registry.js +106 -6
  31. package/dist/src/mcpl/channel-registry.js.map +1 -1
  32. package/dist/src/mcpl/conversation-router.d.ts +8 -6
  33. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  34. package/dist/src/mcpl/conversation-router.js +12 -6
  35. package/dist/src/mcpl/conversation-router.js.map +1 -1
  36. package/dist/src/mcpl/index.d.ts +3 -0
  37. package/dist/src/mcpl/index.d.ts.map +1 -1
  38. package/dist/src/mcpl/index.js +3 -0
  39. package/dist/src/mcpl/index.js.map +1 -1
  40. package/dist/src/mcpl/push-coalescer.d.ts +318 -0
  41. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -0
  42. package/dist/src/mcpl/push-coalescer.js +709 -0
  43. package/dist/src/mcpl/push-coalescer.js.map +1 -0
  44. package/dist/src/mcpl/push-handler.d.ts +7 -2
  45. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  46. package/dist/src/mcpl/push-handler.js +43 -4
  47. package/dist/src/mcpl/push-handler.js.map +1 -1
  48. package/dist/src/mcpl/references.d.ts.map +1 -1
  49. package/dist/src/mcpl/references.js +9 -4
  50. package/dist/src/mcpl/references.js.map +1 -1
  51. package/dist/src/mcpl/server-connection.d.ts +18 -0
  52. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  53. package/dist/src/mcpl/server-connection.js +44 -1
  54. package/dist/src/mcpl/server-connection.js.map +1 -1
  55. package/dist/src/mcpl/tool-classes.d.ts +71 -0
  56. package/dist/src/mcpl/tool-classes.d.ts.map +1 -0
  57. package/dist/src/mcpl/tool-classes.js +170 -0
  58. package/dist/src/mcpl/tool-classes.js.map +1 -0
  59. package/dist/src/mcpl/tool-glob.d.ts +16 -0
  60. package/dist/src/mcpl/tool-glob.d.ts.map +1 -0
  61. package/dist/src/mcpl/tool-glob.js +41 -0
  62. package/dist/src/mcpl/tool-glob.js.map +1 -0
  63. package/dist/src/mcpl/tool-lifecycle.d.ts +253 -0
  64. package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -0
  65. package/dist/src/mcpl/tool-lifecycle.js +607 -0
  66. package/dist/src/mcpl/tool-lifecycle.js.map +1 -0
  67. package/dist/src/mcpl/transport.d.ts +15 -0
  68. package/dist/src/mcpl/transport.d.ts.map +1 -1
  69. package/dist/src/mcpl/transport.js +65 -1
  70. package/dist/src/mcpl/transport.js.map +1 -1
  71. package/dist/src/mcpl/types.d.ts +68 -1
  72. package/dist/src/mcpl/types.d.ts.map +1 -1
  73. package/dist/src/mcpl/types.js +6 -0
  74. package/dist/src/mcpl/types.js.map +1 -1
  75. package/dist/src/modules/discord/index.js +1 -1
  76. package/dist/src/modules/discord/index.js.map +1 -1
  77. package/dist/src/modules/history/index.d.ts +85 -0
  78. package/dist/src/modules/history/index.d.ts.map +1 -1
  79. package/dist/src/modules/history/index.js +1019 -24
  80. package/dist/src/modules/history/index.js.map +1 -1
  81. package/dist/src/modules/history/semantic.d.ts +202 -0
  82. package/dist/src/modules/history/semantic.d.ts.map +1 -0
  83. package/dist/src/modules/history/semantic.js +418 -0
  84. package/dist/src/modules/history/semantic.js.map +1 -0
  85. package/dist/src/modules/index.d.ts +2 -1
  86. package/dist/src/modules/index.d.ts.map +1 -1
  87. package/dist/src/modules/index.js.map +1 -1
  88. package/dist/src/tool-result-guard.d.ts +82 -0
  89. package/dist/src/tool-result-guard.d.ts.map +1 -0
  90. package/dist/src/tool-result-guard.js +306 -0
  91. package/dist/src/tool-result-guard.js.map +1 -0
  92. package/dist/src/types/agent.d.ts +14 -0
  93. package/dist/src/types/agent.d.ts.map +1 -1
  94. package/dist/src/types/framework.d.ts +21 -0
  95. package/dist/src/types/framework.d.ts.map +1 -1
  96. package/dist/src/types/trace.d.ts +17 -0
  97. package/dist/src/types/trace.d.ts.map +1 -1
  98. package/dist/test/channel-registry-routing.test.js +59 -0
  99. package/dist/test/channel-registry-routing.test.js.map +1 -1
  100. package/dist/test/code-execution.test.js +139 -0
  101. package/dist/test/code-execution.test.js.map +1 -1
  102. package/dist/test/conversation-router.test.js +33 -19
  103. package/dist/test/conversation-router.test.js.map +1 -1
  104. package/dist/test/conversation-routing.test.js +89 -0
  105. package/dist/test/conversation-routing.test.js.map +1 -1
  106. package/dist/test/helpers/coalescing-fixture.d.ts +72 -0
  107. package/dist/test/helpers/coalescing-fixture.d.ts.map +1 -0
  108. package/dist/test/helpers/coalescing-fixture.js +145 -0
  109. package/dist/test/helpers/coalescing-fixture.js.map +1 -0
  110. package/dist/test/helpers/coalescing-kill-child.d.ts +2 -0
  111. package/dist/test/helpers/coalescing-kill-child.d.ts.map +1 -0
  112. package/dist/test/helpers/coalescing-kill-child.js +24 -0
  113. package/dist/test/helpers/coalescing-kill-child.js.map +1 -0
  114. package/dist/test/history-module-realstore.test.d.ts +2 -0
  115. package/dist/test/history-module-realstore.test.d.ts.map +1 -0
  116. package/dist/test/history-module-realstore.test.js +363 -0
  117. package/dist/test/history-module-realstore.test.js.map +1 -0
  118. package/dist/test/history-module-semantic.test.d.ts +2 -0
  119. package/dist/test/history-module-semantic.test.d.ts.map +1 -0
  120. package/dist/test/history-module-semantic.test.js +213 -0
  121. package/dist/test/history-module-semantic.test.js.map +1 -0
  122. package/dist/test/history-module-ux.test.d.ts +2 -0
  123. package/dist/test/history-module-ux.test.d.ts.map +1 -0
  124. package/dist/test/history-module-ux.test.js +304 -0
  125. package/dist/test/history-module-ux.test.js.map +1 -0
  126. package/dist/test/history-module.test.js +25 -1
  127. package/dist/test/history-module.test.js.map +1 -1
  128. package/dist/test/history-semantic-review-findings.test.d.ts +2 -0
  129. package/dist/test/history-semantic-review-findings.test.d.ts.map +1 -0
  130. package/dist/test/history-semantic-review-findings.test.js +338 -0
  131. package/dist/test/history-semantic-review-findings.test.js.map +1 -0
  132. package/dist/test/history-semantic-sync-converges.test.d.ts +2 -0
  133. package/dist/test/history-semantic-sync-converges.test.d.ts.map +1 -0
  134. package/dist/test/history-semantic-sync-converges.test.js +109 -0
  135. package/dist/test/history-semantic-sync-converges.test.js.map +1 -0
  136. package/dist/test/mcpl-capability-grant.test.js +3 -1
  137. package/dist/test/mcpl-capability-grant.test.js.map +1 -1
  138. package/dist/test/mcpl-child-env.test.d.ts +2 -0
  139. package/dist/test/mcpl-child-env.test.d.ts.map +1 -0
  140. package/dist/test/mcpl-child-env.test.js +158 -0
  141. package/dist/test/mcpl-child-env.test.js.map +1 -0
  142. package/dist/test/mcpl-coalescing.test.d.ts +2 -0
  143. package/dist/test/mcpl-coalescing.test.d.ts.map +1 -0
  144. package/dist/test/mcpl-coalescing.test.js +1050 -0
  145. package/dist/test/mcpl-coalescing.test.js.map +1 -0
  146. package/dist/test/mcpl-list-status.test.js +4 -0
  147. package/dist/test/mcpl-list-status.test.js.map +1 -1
  148. package/dist/test/mcpl-reference-fields.test.d.ts +2 -0
  149. package/dist/test/mcpl-reference-fields.test.d.ts.map +1 -0
  150. package/dist/test/mcpl-reference-fields.test.js +82 -0
  151. package/dist/test/mcpl-reference-fields.test.js.map +1 -0
  152. package/dist/test/mcpl-tool-class-listing.test.d.ts +2 -0
  153. package/dist/test/mcpl-tool-class-listing.test.d.ts.map +1 -0
  154. package/dist/test/mcpl-tool-class-listing.test.js +147 -0
  155. package/dist/test/mcpl-tool-class-listing.test.js.map +1 -0
  156. package/dist/test/mcpl-tool-classes.test.d.ts +7 -0
  157. package/dist/test/mcpl-tool-classes.test.d.ts.map +1 -0
  158. package/dist/test/mcpl-tool-classes.test.js +80 -0
  159. package/dist/test/mcpl-tool-classes.test.js.map +1 -0
  160. package/dist/test/mcpl-tool-lifecycle-e2e.test.d.ts +2 -0
  161. package/dist/test/mcpl-tool-lifecycle-e2e.test.d.ts.map +1 -0
  162. package/dist/test/mcpl-tool-lifecycle-e2e.test.js +237 -0
  163. package/dist/test/mcpl-tool-lifecycle-e2e.test.js.map +1 -0
  164. package/dist/test/mcpl-tool-lifecycle-emitter.test.d.ts +11 -0
  165. package/dist/test/mcpl-tool-lifecycle-emitter.test.d.ts.map +1 -0
  166. package/dist/test/mcpl-tool-lifecycle-emitter.test.js +307 -0
  167. package/dist/test/mcpl-tool-lifecycle-emitter.test.js.map +1 -0
  168. package/dist/test/mcpl-tool-lifecycle.test.d.ts +11 -0
  169. package/dist/test/mcpl-tool-lifecycle.test.d.ts.map +1 -0
  170. package/dist/test/mcpl-tool-lifecycle.test.js +270 -0
  171. package/dist/test/mcpl-tool-lifecycle.test.js.map +1 -0
  172. package/dist/test/silent-heartbeat.test.d.ts +2 -0
  173. package/dist/test/silent-heartbeat.test.d.ts.map +1 -0
  174. package/dist/test/silent-heartbeat.test.js +154 -0
  175. package/dist/test/silent-heartbeat.test.js.map +1 -0
  176. package/dist/test/tool-result-guard.test.d.ts +2 -0
  177. package/dist/test/tool-result-guard.test.d.ts.map +1 -0
  178. package/dist/test/tool-result-guard.test.js +1081 -0
  179. package/dist/test/tool-result-guard.test.js.map +1 -0
  180. package/dist/test/tools-observe-parser-conformance.test.d.ts +2 -0
  181. package/dist/test/tools-observe-parser-conformance.test.d.ts.map +1 -0
  182. package/dist/test/tools-observe-parser-conformance.test.js +129 -0
  183. package/dist/test/tools-observe-parser-conformance.test.js.map +1 -0
  184. package/dist/test/trunk-channel-routing.test.js +67 -0
  185. package/dist/test/trunk-channel-routing.test.js.map +1 -1
  186. package/dist/tsconfig.tsbuildinfo +1 -1
  187. package/package.json +2 -2
  188. package/src/agent.ts +128 -10
  189. package/src/api/types.ts +2 -0
  190. package/src/code-execution/py-runner.ts +34 -2
  191. package/src/code-execution/tool-definition.ts +46 -0
  192. package/src/framework.ts +1207 -80
  193. package/src/index.ts +13 -0
  194. package/src/mcpl/capability-grant.ts +54 -6
  195. package/src/mcpl/channel-registry.ts +115 -7
  196. package/src/mcpl/conversation-router.ts +12 -8
  197. package/src/mcpl/index.ts +29 -0
  198. package/src/mcpl/push-coalescer.ts +821 -0
  199. package/src/mcpl/push-handler.ts +40 -4
  200. package/src/mcpl/references.ts +11 -3
  201. package/src/mcpl/server-connection.ts +48 -2
  202. package/src/mcpl/tool-classes.ts +188 -0
  203. package/src/mcpl/tool-glob.ts +36 -0
  204. package/src/mcpl/tool-lifecycle.ts +745 -0
  205. package/src/mcpl/transport.ts +67 -1
  206. package/src/mcpl/types.ts +71 -1
  207. package/src/modules/discord/index.ts +1 -1
  208. package/src/modules/history/index.ts +1089 -24
  209. package/src/modules/history/semantic.ts +473 -0
  210. package/src/modules/index.ts +2 -1
  211. package/src/tool-result-guard.ts +318 -0
  212. package/src/types/agent.ts +15 -0
  213. package/src/types/framework.ts +23 -0
  214. package/src/types/trace.ts +17 -0
@@ -18,6 +18,7 @@ import type {
18
18
  import type { FeatureSetManager } from './feature-set-manager.js';
19
19
  import { McplFeatureSetError } from './feature-set-manager.js';
20
20
  import { expandCoreTags } from './tags.js';
21
+ import { validateCoalescedContent } from './push-coalescer.js';
21
22
 
22
23
  // ============================================================================
23
24
  // McplPushEvent (the ProcessEvent shape pushed to the queue)
@@ -42,6 +43,10 @@ export interface McplPushEvent {
42
43
  inferenceId: string;
43
44
  triggerInference?: boolean;
44
45
  targetAgents?: string[];
46
+ /** RFC-006: set when the coalescer delivers; rides the wake it queues. */
47
+ coalescingSubject?: string;
48
+ /** RFC-006 assembly: materialized for this agent's turn (store directly). */
49
+ assemblingFor?: string;
45
50
  }
46
51
 
47
52
  // ============================================================================
@@ -171,6 +176,7 @@ export class PushHandler {
171
176
  pushEventFn: (event: McplPushEvent) => void,
172
177
  emitTraceFn: (event: { type: string; [key: string]: unknown }) => void,
173
178
  shouldTriggerInference?: (content: string, metadata: Record<string, unknown>) => boolean,
179
+ private readonly handleCoalesced?: (serverId: string, params: PushEventParams, event: McplPushEvent) => Promise<PushEventResult>,
174
180
  ) {
175
181
  this.featureSetManager = featureSetManager;
176
182
  this.pushEventFn = pushEventFn;
@@ -189,11 +195,11 @@ export class PushHandler {
189
195
  * 6. Emit trace
190
196
  * 7. Respond with accepted + inferenceId
191
197
  */
192
- handlePushEvent(
198
+ async handlePushEvent(
193
199
  serverId: string,
194
200
  params: PushEventParams,
195
201
  responder?: Responder,
196
- ): void {
202
+ ): Promise<void> {
197
203
  // §16.3: expand the normative chat:* core closure once, at entry, so
198
204
  // every downstream consumer (wake matching, metadata, the queued event)
199
205
  // sees the closed set. Producer `implies` edges are NOT consumed —
@@ -223,8 +229,22 @@ export class PushHandler {
223
229
  return;
224
230
  }
225
231
 
226
- // 2. Deduplicate by eventId
227
- if (this.dedup.checkAndAdd(params.eventId)) {
232
+ // 2. Deduplicate by eventId. A coalesced occurrence is deduplicated by the
233
+ // coalescer's receipts instead (RFC-006 §3.1: a retry within the window
234
+ // gets its original result, which this set could not return).
235
+ const coalesced = params.coalesce !== undefined && !!this.handleCoalesced;
236
+ if (coalesced) {
237
+ // RFC-006 §13: malformed content is a -32602, checked before conversion.
238
+ try {
239
+ validateCoalescedContent(params.payload?.content);
240
+ } catch (error) {
241
+ const err = error as Error & { code?: number; field?: string };
242
+ if (responder?.respondError) responder.respondError(err.code ?? -32602, err.message, { field: err.field });
243
+ else responder?.respond({ accepted: false, reason: err.message });
244
+ return;
245
+ }
246
+ }
247
+ if (!coalesced && this.dedup.checkAndAdd(params.eventId)) {
228
248
  console.error(`[push-event-rejected] server=${serverId} eventId=${params.eventId} reason=duplicate`);
229
249
  responder?.respond({ accepted: false, reason: 'duplicate' });
230
250
  return;
@@ -267,6 +287,22 @@ export class PushHandler {
267
287
  inferenceId,
268
288
  triggerInference,
269
289
  };
290
+ if (coalesced) {
291
+ // RFC-006: the coalescer decides whether this occurrence replaces an
292
+ // unread one, appends, or withdraws; it delivers through the same event
293
+ // the ordinary path would have queued.
294
+ try {
295
+ const result = await this.handleCoalesced!(serverId, params, pushEvent);
296
+ this.emitTraceFn({ type: 'mcpl:push_event', serverId, eventId: params.eventId, featureSet: params.featureSet, coalesce: result.coalesce });
297
+ responder?.respond(result);
298
+ } catch (error) {
299
+ const err = error as Error & { code?: number; field?: string };
300
+ console.error(`[push-event-rejected] server=${serverId} eventId=${params.eventId} reason=${err.message}`);
301
+ if (responder?.respondError) responder.respondError(typeof err.code === 'number' ? err.code : -32603, err.message, { field: err.field });
302
+ else responder?.respond({ accepted: false, reason: err.message });
303
+ }
304
+ return;
305
+ }
270
306
  this.pushEventFn(pushEvent);
271
307
 
272
308
  // 7. Emit trace
@@ -56,12 +56,20 @@ function isRecord(x: unknown): x is Record<string, unknown> {
56
56
 
57
57
  function takeString(
58
58
  raw: unknown, field: keyof typeof REFERENCE_LIMITS & string,
59
- out: { rejected: string[]; truncated: string[] },
59
+ out: { rejected: string[] },
60
60
  ): string | undefined {
61
61
  if (raw === undefined) return undefined;
62
62
  if (typeof raw !== 'string') { out.rejected.push(field); return undefined; }
63
- const limit = REFERENCE_LIMITS[field];
64
- if (raw.length > limit) { out.truncated.push(field); return raw.slice(0, limit); }
63
+ // JSON Schema maxLength counts Unicode code points, not UTF-16 units.
64
+ // Stop as soon as the optional field is invalid, without allocating a copy
65
+ // of an arbitrarily large server string. Display truncation happens later.
66
+ let length = 0;
67
+ for (const _codePoint of raw) {
68
+ if (++length > REFERENCE_LIMITS[field]) {
69
+ out.rejected.push(field);
70
+ return undefined;
71
+ }
72
+ }
65
73
  return raw;
66
74
  }
67
75
 
@@ -46,6 +46,7 @@ import type {
46
46
  } from './types.js';
47
47
 
48
48
  import { McplMethod } from './types.js';
49
+ import type { ToolLifecycleParams, ToolObserveRule } from './tool-lifecycle.js';
49
50
 
50
51
  /** Timeout for the initialize handshake in milliseconds.
51
52
  * Spring Boot + JDA servers can take 5-10s to boot, so 30s is safe. */
@@ -135,6 +136,19 @@ export class McplServerConnection extends EventEmitter {
135
136
  /** True once the §5.3 initial policy Request has been answered. */
136
137
  policyEstablished = false;
137
138
 
139
+ /**
140
+ * RFC-007 §6: the server's `tools/observe` filter — interest, never
141
+ * authority (it only narrows what the grant allows). Null = no filter in
142
+ * force, which means every granted call is reported with METADATA ONLY
143
+ * (§5.1). Per connection epoch: discarded on reconnect and whenever
144
+ * `toolLifecycle.observe` leaves the grant (§6.5).
145
+ */
146
+ toolObserveFilter: ToolObserveRule[] | null = null;
147
+
148
+ /** Increments at every transport boundary. RFC-007 terminals go only to
149
+ * the epoch their opening went to, never across a reconnect. */
150
+ transportEpoch = 0;
151
+
138
152
  /**
139
153
  * §17 manifest tracking (host side of RFC-003): what this host actually
140
154
  * fetched and acted on — never the server's announcement log (§17.10:
@@ -157,6 +171,9 @@ export class McplServerConnection extends EventEmitter {
157
171
  establishGrant(grant: CapabilityGrant): void {
158
172
  this.grant = grant;
159
173
  this.policyEstablished = true;
174
+ // RFC-007 §6.5: revoking observe discards the filter; the server sends
175
+ // it again if the capability is granted later.
176
+ if (!grant.has('toolLifecycle.observe')) this.toolObserveFilter = null;
160
177
  }
161
178
 
162
179
  /** A §5.3 grant belongs to one initialized transport epoch. Reconnecting
@@ -165,6 +182,10 @@ export class McplServerConnection extends EventEmitter {
165
182
  private resetPolicyForTransportBoundary(): void {
166
183
  this.grant = CapabilityGrant.empty();
167
184
  this.policyEstablished = false;
185
+ this.transportEpoch++;
186
+ // RFC-007 §6.5: a filter is per connection epoch — after a reconnect the
187
+ // server sends it again, and until then receives metadata only.
188
+ this.toolObserveFilter = null;
168
189
  // §17 tracking is per-epoch for the same reason the grant is: these are
169
190
  // facts about what THIS host fetched/negotiated on THIS initialized
170
191
  // transport, and a fresh initialize starts a fresh manifest history
@@ -186,7 +207,7 @@ export class McplServerConnection extends EventEmitter {
186
207
  private pendingRequests = new Map<string | number, PendingRequest>();
187
208
  /** Metadata retained after selected request deadlines so a late response is
188
209
  * observable even though its original promise has already been rejected. */
189
- private orphanedRequests = new Map<string | number, { method: string }>();
210
+ private orphanedRequests = new Map<string | number, { method: string; renderParams?: Record<string, unknown> }>();
190
211
  private closed = false;
191
212
 
192
213
  /** Per-request timeout in ms (0 disables). See McplServerConfig.requestTimeoutMs. */
@@ -402,6 +423,9 @@ export class McplServerConnection extends EventEmitter {
402
423
  'channels-changed': 'channels.register',
403
424
  'channels-incoming': 'channels.incoming',
404
425
  'channels-list': 'channels.register',
426
+ // RFC-007 §4.1: tools/observe is accepted only under toolLifecycle.observe
427
+ // (-32002 otherwise, including before the §5.3 policy exchange).
428
+ 'tools-observe': 'toolLifecycle.observe',
405
429
  };
406
430
 
407
431
  private static isControlPlaneEvent(event: string): boolean {
@@ -410,6 +434,9 @@ export class McplServerConnection extends EventEmitter {
410
434
  || event === 'channels-register'
411
435
  || event === 'channels-changed'
412
436
  || event === 'tools-list-changed'
437
+ // RFC-007: a filter is policy-shaped and only narrows; applying it
438
+ // while the data plane is paused can only reduce what is sent.
439
+ || event === 'tools-observe'
413
440
  || event === 'connect-failed'
414
441
  || event === 'reconnect-failed'
415
442
  || event === 'orphaned-response'
@@ -629,6 +656,11 @@ export class McplServerConnection extends EventEmitter {
629
656
  return this.sendRequest(McplMethod.BeforeInference, params as unknown as Record<string, unknown>) as Promise<BeforeInferenceResult>;
630
657
  }
631
658
 
659
+ /** RFC-006 deferred push rendering; its own timeout bounds context assembly. */
660
+ sendPushRender(params: import('./push-coalescer.js').PushRenderParams): Promise<import('./push-coalescer.js').PushRenderResult> {
661
+ return this.sendRequest('push/render', params as unknown as Record<string, unknown>, { timeoutMs: 5000, surfaceOrphanedResponse: true }) as Promise<import('./push-coalescer.js').PushRenderResult>;
662
+ }
663
+
632
664
 
633
665
  /** Send `featureSets/update` as a Notification. §6.7: valid ONLY for
634
666
  * purely descriptive metadata that does not alter the grant. Any grant
@@ -666,6 +698,13 @@ export class McplServerConnection extends EventEmitter {
666
698
  this.sendNotification(McplMethod.InferenceLifecycle, params as unknown as Record<string, unknown>);
667
699
  }
668
700
 
701
+ /** Send `tools/lifecycle` notification (RFC-007). Best-effort. The
702
+ * caller (ToolLifecycleEmitter) has already applied grant, narrowing,
703
+ * class exclusions and this connection's filter. */
704
+ sendToolLifecycle(params: ToolLifecycleParams): void {
705
+ this.sendNotification(McplMethod.ToolsLifecycle, params as unknown as Record<string, unknown>);
706
+ }
707
+
669
708
  /**
670
709
  * Fetch the server's complete current manifest (§17.4) — the
671
710
  * experimental.mcpl object in the same shape initialize carries, never a
@@ -952,7 +991,8 @@ export class McplServerConnection extends EventEmitter {
952
991
  pending.timer = setTimeout(() => {
953
992
  this.pendingRequests.delete(id);
954
993
  if (options.surfaceOrphanedResponse) {
955
- this.orphanedRequests.set(id, { method });
994
+ this.orphanedRequests.set(id, { method, ...(method === 'push/render' ? { renderParams: { featureSet: params.featureSet, key: params.key, eventId: params.eventId } } : {}) });
995
+ if (this.orphanedRequests.size > 4096) this.orphanedRequests.delete(this.orphanedRequests.keys().next().value!);
956
996
  }
957
997
  reject(new Error(
958
998
  `MCPL server "${this.id}" did not respond to ${method} (id=${id}) ` +
@@ -1012,6 +1052,7 @@ export class McplServerConnection extends EventEmitter {
1012
1052
  [McplMethod.ManifestChanged]: 'manifest-changed',
1013
1053
  [McplMethod.ChannelsList]: 'channels-list',
1014
1054
  'notifications/tools/list_changed': 'tools-list-changed',
1055
+ [McplMethod.ToolsObserve]: 'tools-observe',
1015
1056
  // Host-level admin commands initiated from a surface (e.g. a Discord
1016
1057
  // slash command). Params: { command, ... }; host responds with a
1017
1058
  // command-specific result object.
@@ -1043,6 +1084,7 @@ export class McplServerConnection extends EventEmitter {
1043
1084
  [McplMethod.ChannelsChanged]: 'channels.register',
1044
1085
  [McplMethod.ChannelsIncoming]: 'channels.incoming',
1045
1086
  [McplMethod.ChannelsList]: 'channels.register',
1087
+ [McplMethod.ToolsObserve]: 'toolLifecycle.observe',
1046
1088
  };
1047
1089
 
1048
1090
  /**
@@ -1142,6 +1184,10 @@ export class McplServerConnection extends EventEmitter {
1142
1184
  // re-inject either result because the original dispatch context is gone.
1143
1185
  const orphaned = this.orphanedRequests.get(response.id);
1144
1186
  this.orphanedRequests.delete(response.id);
1187
+ if (orphaned?.renderParams) {
1188
+ this.emit('orphaned-render-response', { params: orphaned.renderParams, result: response.result, error: response.error });
1189
+ return; // audit only: a timeout fallback can never be replaced
1190
+ }
1145
1191
  const result = response.result;
1146
1192
  if (result && typeof result === 'object') {
1147
1193
  const r = result as { state?: unknown; checkpoint?: unknown };
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Tool classes (MCPL RFC-008) — a coarse, fixed vocabulary for what a tool
3
+ * DOES, used as a key for host policy (RFC-007's argument exclusions today;
4
+ * approvals, audit grouping and caps later).
5
+ *
6
+ * A class is a HINT at the tier of MCP `annotations`, never a grant: it
7
+ * confers no authority, gates no method, and never changes what the model
8
+ * sees. The providing server declares it on its MCP tool definition as
9
+ * `_meta["mcpl/class"]`; the host computes an EFFECTIVE class per tool from
10
+ * (in precedence order, first source that yields a class wins entirely):
11
+ *
12
+ * 1. operator override (FrameworkConfig.toolClassOverrides),
13
+ * 2. the host's own knowledge of tools it implements (BUILTIN_TOOL_CLASSES
14
+ * plus FrameworkConfig.hostToolClasses for an embedding host's modules),
15
+ * 3. the server's declaration.
16
+ *
17
+ * An UNCLASSED tool is handled as the most restrictive class for every
18
+ * question asked of it (RFC-008 §5.2): classing is how a tool earns looser
19
+ * handling, never how it is denied. A tool with several classes gets the
20
+ * union of their restrictions (§5.3).
21
+ */
22
+
23
+ import { globMatch } from './tool-glob.js';
24
+
25
+ export const TOOL_CLASSES = [
26
+ 'comms',
27
+ 'memory',
28
+ 'notes',
29
+ 'files',
30
+ 'shell',
31
+ 'web',
32
+ 'computer',
33
+ 'media',
34
+ 'body',
35
+ 'control',
36
+ ] as const;
37
+
38
+ export type ToolClass = (typeof TOOL_CLASSES)[number];
39
+
40
+ const CLASS_SET: ReadonlySet<string> = new Set(TOOL_CLASSES);
41
+
42
+ export function isToolClass(value: unknown): value is ToolClass {
43
+ return typeof value === 'string' && CLASS_SET.has(value);
44
+ }
45
+
46
+ /** The `_meta` key a server declares a tool's class under (RFC-008 §3). */
47
+ export const TOOL_CLASS_META_KEY = 'mcpl/class';
48
+
49
+ /**
50
+ * Classes whose arguments are ordinarily safe to show an observer (RFC-007
51
+ * §4.3's recommended default `inputs` policy). `memory`, `notes` and
52
+ * `control` are left to explicit operator choice; `comms` is never a choice.
53
+ */
54
+ export const DEFAULT_INPUT_CLASSES: readonly ToolClass[] = [
55
+ 'computer',
56
+ 'shell',
57
+ 'files',
58
+ 'web',
59
+ 'media',
60
+ 'body',
61
+ ];
62
+
63
+ /**
64
+ * Classes of the tools this framework implements itself — the host's own
65
+ * knowledge (RFC-008 §5.1 source 2). Keys use the RFC-007 §6.2 pattern
66
+ * grammar over the model-facing tool name. Anything not listed here, not
67
+ * overridden and not declared by its server is UNCLASSED, which is the
68
+ * restrictive answer — so an omission here fails closed.
69
+ */
70
+ export const BUILTIN_TOOL_CLASSES: Readonly<Record<string, readonly ToolClass[]>> = {
71
+ // Synthesized channel tools: speech to and from people (RFC-008 §4:
72
+ // speech that reaches a person is comms, wherever it goes).
73
+ 'channel_*': ['comms'],
74
+ // Deliberately staying silent in a conversation is still about that
75
+ // conversation; its reason text can quote it.
76
+ skip_reply: ['comms'],
77
+ // The agent's private interior.
78
+ think: ['memory'],
79
+ journal: ['memory'],
80
+ // Host and agent settings, wake policy, routing help.
81
+ utils: ['control'],
82
+ agent_settings: ['control'],
83
+ gate_status: ['control'],
84
+ tune_out: ['control'],
85
+ sleep: ['control'],
86
+ wake: ['control'],
87
+ wake_add_rule: ['control'],
88
+ wake_remove_rule: ['control'],
89
+ event_tags: ['control'],
90
+ prose_help: ['control'],
91
+ // Files and blobs in the agent's own stores.
92
+ save_recent_image: ['files'],
93
+ read_image: ['files'],
94
+ // RFC-005 references resolve to the agent's own or private documents, not
95
+ // the public network (RFC-008 §4: private fetches are files/notes).
96
+ fetch_reference: ['files'],
97
+ // Runs code that may do anything: the class describes the tool.
98
+ code_execution: ['shell'],
99
+ // Built-in modules.
100
+ 'discord--*': ['comms'],
101
+ 'history--*': ['memory'],
102
+ 'workspace--*': ['files'],
103
+ 'health--*': ['control'],
104
+ 'api--*': ['control'],
105
+ };
106
+
107
+ /**
108
+ * Read a server's class declaration from a tool definition's `_meta`.
109
+ * Returns the known classes it names (deduplicated, in order), or undefined
110
+ * when it names none — which makes the tool unclassed. Unknown strings are
111
+ * ignored for policy and logged (RFC-008 §3, vectors 3 and 4); a non-array
112
+ * value is malformed and ignored the same way.
113
+ */
114
+ export function parseDeclaredClasses(meta: unknown, toolName = '(unnamed)'): ToolClass[] | undefined {
115
+ if (!meta || typeof meta !== 'object') return undefined;
116
+ const raw = (meta as Record<string, unknown>)[TOOL_CLASS_META_KEY];
117
+ if (raw === undefined) return undefined;
118
+ if (!Array.isArray(raw)) {
119
+ console.error(`[mcpl] ${toolName}: _meta["${TOOL_CLASS_META_KEY}"] is not an array — treated as unclassed`);
120
+ return undefined;
121
+ }
122
+ const out: ToolClass[] = [];
123
+ for (const value of raw) {
124
+ if (isToolClass(value)) {
125
+ if (!out.includes(value)) out.push(value);
126
+ } else {
127
+ console.error(`[mcpl] ${toolName}: unknown tool class ${JSON.stringify(value)} ignored (RFC-008 §3)`);
128
+ }
129
+ }
130
+ return out.length > 0 ? out : undefined;
131
+ }
132
+
133
+ /**
134
+ * Validate a pattern → classes table from configuration. Unknown class
135
+ * strings are dropped with a diagnostic; an entry left with no class is
136
+ * dropped entirely (it cannot classify anything).
137
+ */
138
+ export function normalizeClassTable(
139
+ table: Record<string, readonly string[]> | undefined,
140
+ label: string,
141
+ ): Array<[string, ToolClass[]]> {
142
+ if (!table) return [];
143
+ const out: Array<[string, ToolClass[]]> = [];
144
+ for (const [pattern, classes] of Object.entries(table)) {
145
+ const known: ToolClass[] = [];
146
+ for (const c of Array.isArray(classes) ? classes : []) {
147
+ if (isToolClass(c)) {
148
+ if (!known.includes(c)) known.push(c);
149
+ } else {
150
+ console.error(`[mcpl] ${label}["${pattern}"]: unknown tool class ${JSON.stringify(c)} ignored`);
151
+ }
152
+ }
153
+ if (known.length > 0) out.push([pattern, known]);
154
+ }
155
+ return out;
156
+ }
157
+
158
+ export type ToolClassSource = 'override' | 'host' | 'server' | 'none';
159
+
160
+ export interface EffectiveToolClass {
161
+ /** Empty for an unclassed tool. */
162
+ classes: ToolClass[];
163
+ source: ToolClassSource;
164
+ }
165
+
166
+ /**
167
+ * Compute a tool's effective class (RFC-008 §5.1). `overrides` and `host`
168
+ * are ordered pattern tables (first matching pattern wins within a source);
169
+ * the first SOURCE that yields a class wins entirely — sources never merge,
170
+ * so an override can tighten or correct a server's declaration.
171
+ */
172
+ export function resolveToolClass(
173
+ toolName: string,
174
+ declared: readonly ToolClass[] | undefined,
175
+ tables: {
176
+ overrides: ReadonlyArray<readonly [string, readonly ToolClass[]]>;
177
+ host: ReadonlyArray<readonly [string, readonly ToolClass[]]>;
178
+ },
179
+ ): EffectiveToolClass {
180
+ for (const [pattern, classes] of tables.overrides) {
181
+ if (globMatch(pattern, toolName)) return { classes: [...classes], source: 'override' };
182
+ }
183
+ for (const [pattern, classes] of tables.host) {
184
+ if (globMatch(pattern, toolName)) return { classes: [...classes], source: 'host' };
185
+ }
186
+ if (declared && declared.length > 0) return { classes: [...declared], source: 'server' };
187
+ return { classes: [], source: 'none' };
188
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The portable pattern grammar of MCPL RFC-007 §6.2, used for `tools/observe`
3
+ * filters and (RFC-007 §17 open question 2) for this host's own tool-lifecycle
4
+ * narrowing and tool-class tables:
5
+ *
6
+ * - `*` matches any sequence of zero or more characters;
7
+ * - every other character matches itself (no escapes, no other metachars);
8
+ * - matching is against the whole string, case-sensitive.
9
+ *
10
+ * Patterns are server-supplied, so matching must not backtrack without bound
11
+ * (RFC-007 §10 "Matching cost"). This is the classic two-pointer wildcard
12
+ * match: it remembers only the most recent `*`, so it runs in
13
+ * O(pattern × subject) worst case and never recurses.
14
+ */
15
+ export function globMatch(pattern: string, subject: string): boolean {
16
+ let p = 0;
17
+ let s = 0;
18
+ let star = -1;
19
+ let mark = 0;
20
+ while (s < subject.length) {
21
+ if (p < pattern.length && pattern[p] !== '*' && pattern[p] === subject[s]) {
22
+ p++;
23
+ s++;
24
+ } else if (p < pattern.length && pattern[p] === '*') {
25
+ star = p++;
26
+ mark = s;
27
+ } else if (star !== -1) {
28
+ p = star + 1;
29
+ s = ++mark;
30
+ } else {
31
+ return false;
32
+ }
33
+ }
34
+ while (p < pattern.length && pattern[p] === '*') p++;
35
+ return p === pattern.length;
36
+ }