@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.
- package/README.md +29 -0
- package/dist/src/agent.d.ts +22 -0
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +128 -10
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/types.d.ts +1 -1
- package/dist/src/api/types.d.ts.map +1 -1
- package/dist/src/code-execution/py-runner.d.ts +10 -1
- package/dist/src/code-execution/py-runner.d.ts.map +1 -1
- package/dist/src/code-execution/py-runner.js +27 -2
- package/dist/src/code-execution/py-runner.js.map +1 -1
- package/dist/src/code-execution/tool-definition.d.ts +21 -0
- package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
- package/dist/src/code-execution/tool-definition.js +34 -0
- package/dist/src/code-execution/tool-definition.js.map +1 -1
- package/dist/src/framework.d.ts +153 -2
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1175 -76
- package/dist/src/framework.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/capability-grant.d.ts +1 -1
- package/dist/src/mcpl/capability-grant.d.ts.map +1 -1
- package/dist/src/mcpl/capability-grant.js +49 -5
- package/dist/src/mcpl/capability-grant.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +59 -2
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +106 -6
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +8 -6
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +12 -6
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/index.d.ts +3 -0
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js +3 -0
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +318 -0
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -0
- package/dist/src/mcpl/push-coalescer.js +709 -0
- package/dist/src/mcpl/push-coalescer.js.map +1 -0
- package/dist/src/mcpl/push-handler.d.ts +7 -2
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +43 -4
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +9 -4
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +18 -0
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +44 -1
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-classes.d.ts +71 -0
- package/dist/src/mcpl/tool-classes.d.ts.map +1 -0
- package/dist/src/mcpl/tool-classes.js +170 -0
- package/dist/src/mcpl/tool-classes.js.map +1 -0
- package/dist/src/mcpl/tool-glob.d.ts +16 -0
- package/dist/src/mcpl/tool-glob.d.ts.map +1 -0
- package/dist/src/mcpl/tool-glob.js +41 -0
- package/dist/src/mcpl/tool-glob.js.map +1 -0
- package/dist/src/mcpl/tool-lifecycle.d.ts +253 -0
- package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -0
- package/dist/src/mcpl/tool-lifecycle.js +607 -0
- package/dist/src/mcpl/tool-lifecycle.js.map +1 -0
- package/dist/src/mcpl/transport.d.ts +15 -0
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +65 -1
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +68 -1
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js +6 -0
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/modules/discord/index.js +1 -1
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/history/index.d.ts +85 -0
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +1019 -24
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/history/semantic.d.ts +202 -0
- package/dist/src/modules/history/semantic.d.ts.map +1 -0
- package/dist/src/modules/history/semantic.js +418 -0
- package/dist/src/modules/history/semantic.js.map +1 -0
- package/dist/src/modules/index.d.ts +2 -1
- package/dist/src/modules/index.d.ts.map +1 -1
- package/dist/src/modules/index.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts +82 -0
- package/dist/src/tool-result-guard.d.ts.map +1 -0
- package/dist/src/tool-result-guard.js +306 -0
- package/dist/src/tool-result-guard.js.map +1 -0
- package/dist/src/types/agent.d.ts +14 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +21 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +17 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/channel-registry-routing.test.js +59 -0
- package/dist/test/channel-registry-routing.test.js.map +1 -1
- package/dist/test/code-execution.test.js +139 -0
- package/dist/test/code-execution.test.js.map +1 -1
- package/dist/test/conversation-router.test.js +33 -19
- package/dist/test/conversation-router.test.js.map +1 -1
- package/dist/test/conversation-routing.test.js +89 -0
- package/dist/test/conversation-routing.test.js.map +1 -1
- package/dist/test/helpers/coalescing-fixture.d.ts +72 -0
- package/dist/test/helpers/coalescing-fixture.d.ts.map +1 -0
- package/dist/test/helpers/coalescing-fixture.js +145 -0
- package/dist/test/helpers/coalescing-fixture.js.map +1 -0
- package/dist/test/helpers/coalescing-kill-child.d.ts +2 -0
- package/dist/test/helpers/coalescing-kill-child.d.ts.map +1 -0
- package/dist/test/helpers/coalescing-kill-child.js +24 -0
- package/dist/test/helpers/coalescing-kill-child.js.map +1 -0
- package/dist/test/history-module-realstore.test.d.ts +2 -0
- package/dist/test/history-module-realstore.test.d.ts.map +1 -0
- package/dist/test/history-module-realstore.test.js +363 -0
- package/dist/test/history-module-realstore.test.js.map +1 -0
- package/dist/test/history-module-semantic.test.d.ts +2 -0
- package/dist/test/history-module-semantic.test.d.ts.map +1 -0
- package/dist/test/history-module-semantic.test.js +213 -0
- package/dist/test/history-module-semantic.test.js.map +1 -0
- package/dist/test/history-module-ux.test.d.ts +2 -0
- package/dist/test/history-module-ux.test.d.ts.map +1 -0
- package/dist/test/history-module-ux.test.js +304 -0
- package/dist/test/history-module-ux.test.js.map +1 -0
- package/dist/test/history-module.test.js +25 -1
- package/dist/test/history-module.test.js.map +1 -1
- package/dist/test/history-semantic-review-findings.test.d.ts +2 -0
- package/dist/test/history-semantic-review-findings.test.d.ts.map +1 -0
- package/dist/test/history-semantic-review-findings.test.js +338 -0
- package/dist/test/history-semantic-review-findings.test.js.map +1 -0
- package/dist/test/history-semantic-sync-converges.test.d.ts +2 -0
- package/dist/test/history-semantic-sync-converges.test.d.ts.map +1 -0
- package/dist/test/history-semantic-sync-converges.test.js +109 -0
- package/dist/test/history-semantic-sync-converges.test.js.map +1 -0
- package/dist/test/mcpl-capability-grant.test.js +3 -1
- package/dist/test/mcpl-capability-grant.test.js.map +1 -1
- package/dist/test/mcpl-child-env.test.d.ts +2 -0
- package/dist/test/mcpl-child-env.test.d.ts.map +1 -0
- package/dist/test/mcpl-child-env.test.js +158 -0
- package/dist/test/mcpl-child-env.test.js.map +1 -0
- package/dist/test/mcpl-coalescing.test.d.ts +2 -0
- package/dist/test/mcpl-coalescing.test.d.ts.map +1 -0
- package/dist/test/mcpl-coalescing.test.js +1050 -0
- package/dist/test/mcpl-coalescing.test.js.map +1 -0
- package/dist/test/mcpl-list-status.test.js +4 -0
- package/dist/test/mcpl-list-status.test.js.map +1 -1
- package/dist/test/mcpl-reference-fields.test.d.ts +2 -0
- package/dist/test/mcpl-reference-fields.test.d.ts.map +1 -0
- package/dist/test/mcpl-reference-fields.test.js +82 -0
- package/dist/test/mcpl-reference-fields.test.js.map +1 -0
- package/dist/test/mcpl-tool-class-listing.test.d.ts +2 -0
- package/dist/test/mcpl-tool-class-listing.test.d.ts.map +1 -0
- package/dist/test/mcpl-tool-class-listing.test.js +147 -0
- package/dist/test/mcpl-tool-class-listing.test.js.map +1 -0
- package/dist/test/mcpl-tool-classes.test.d.ts +7 -0
- package/dist/test/mcpl-tool-classes.test.d.ts.map +1 -0
- package/dist/test/mcpl-tool-classes.test.js +80 -0
- package/dist/test/mcpl-tool-classes.test.js.map +1 -0
- package/dist/test/mcpl-tool-lifecycle-e2e.test.d.ts +2 -0
- package/dist/test/mcpl-tool-lifecycle-e2e.test.d.ts.map +1 -0
- package/dist/test/mcpl-tool-lifecycle-e2e.test.js +237 -0
- package/dist/test/mcpl-tool-lifecycle-e2e.test.js.map +1 -0
- package/dist/test/mcpl-tool-lifecycle-emitter.test.d.ts +11 -0
- package/dist/test/mcpl-tool-lifecycle-emitter.test.d.ts.map +1 -0
- package/dist/test/mcpl-tool-lifecycle-emitter.test.js +307 -0
- package/dist/test/mcpl-tool-lifecycle-emitter.test.js.map +1 -0
- package/dist/test/mcpl-tool-lifecycle.test.d.ts +11 -0
- package/dist/test/mcpl-tool-lifecycle.test.d.ts.map +1 -0
- package/dist/test/mcpl-tool-lifecycle.test.js +270 -0
- package/dist/test/mcpl-tool-lifecycle.test.js.map +1 -0
- package/dist/test/silent-heartbeat.test.d.ts +2 -0
- package/dist/test/silent-heartbeat.test.d.ts.map +1 -0
- package/dist/test/silent-heartbeat.test.js +154 -0
- package/dist/test/silent-heartbeat.test.js.map +1 -0
- package/dist/test/tool-result-guard.test.d.ts +2 -0
- package/dist/test/tool-result-guard.test.d.ts.map +1 -0
- package/dist/test/tool-result-guard.test.js +1081 -0
- package/dist/test/tool-result-guard.test.js.map +1 -0
- package/dist/test/tools-observe-parser-conformance.test.d.ts +2 -0
- package/dist/test/tools-observe-parser-conformance.test.d.ts.map +1 -0
- package/dist/test/tools-observe-parser-conformance.test.js +129 -0
- package/dist/test/tools-observe-parser-conformance.test.js.map +1 -0
- package/dist/test/trunk-channel-routing.test.js +67 -0
- package/dist/test/trunk-channel-routing.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/agent.ts +128 -10
- package/src/api/types.ts +2 -0
- package/src/code-execution/py-runner.ts +34 -2
- package/src/code-execution/tool-definition.ts +46 -0
- package/src/framework.ts +1207 -80
- package/src/index.ts +13 -0
- package/src/mcpl/capability-grant.ts +54 -6
- package/src/mcpl/channel-registry.ts +115 -7
- package/src/mcpl/conversation-router.ts +12 -8
- package/src/mcpl/index.ts +29 -0
- package/src/mcpl/push-coalescer.ts +821 -0
- package/src/mcpl/push-handler.ts +40 -4
- package/src/mcpl/references.ts +11 -3
- package/src/mcpl/server-connection.ts +48 -2
- package/src/mcpl/tool-classes.ts +188 -0
- package/src/mcpl/tool-glob.ts +36 -0
- package/src/mcpl/tool-lifecycle.ts +745 -0
- package/src/mcpl/transport.ts +67 -1
- package/src/mcpl/types.ts +71 -1
- package/src/modules/discord/index.ts +1 -1
- package/src/modules/history/index.ts +1089 -24
- package/src/modules/history/semantic.ts +473 -0
- package/src/modules/index.ts +2 -1
- package/src/tool-result-guard.ts +318 -0
- package/src/types/agent.ts +15 -0
- package/src/types/framework.ts +23 -0
- package/src/types/trace.ts +17 -0
package/src/mcpl/push-handler.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
package/src/mcpl/references.ts
CHANGED
|
@@ -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[]
|
|
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
|
-
|
|
64
|
-
|
|
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
|
+
}
|