@animalabs/agent-framework 0.18.0 → 0.20.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 +161 -7
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1183 -87
- 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 +74 -2
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +156 -10
- package/dist/src/mcpl/channel-registry.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/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 +592 -0
- package/dist/src/mcpl/tool-lifecycle.js.map +1 -0
- package/dist/src/mcpl/transport.d.ts +12 -0
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +33 -1
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +66 -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/channels-changed-respond-first.test.d.ts +2 -0
- package/dist/test/channels-changed-respond-first.test.d.ts.map +1 -0
- package/dist/test/channels-changed-respond-first.test.js +74 -0
- package/dist/test/channels-changed-respond-first.test.js.map +1 -0
- package/dist/test/code-execution.test.js +139 -0
- package/dist/test/code-execution.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/journal-tool.test.d.ts +2 -0
- package/dist/test/journal-tool.test.d.ts.map +1 -0
- package/dist/test/journal-tool.test.js +33 -0
- package/dist/test/journal-tool.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 +69 -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-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/overbudget-drain-breaker.test.js +44 -4
- package/dist/test/overbudget-drain-breaker.test.js.map +1 -1
- 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/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 +1215 -91
- package/src/index.ts +13 -0
- package/src/mcpl/capability-grant.ts +54 -6
- package/src/mcpl/channel-registry.ts +170 -10
- 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/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 +730 -0
- package/src/mcpl/transport.ts +35 -1
- package/src/mcpl/types.ts +69 -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/index.ts
CHANGED
|
@@ -93,6 +93,19 @@ export type { McplServerConfig } from './mcpl/index.js';
|
|
|
93
93
|
export { McplServerConnection } from './mcpl/index.js';
|
|
94
94
|
export type { McplHostCapabilities } from './mcpl/index.js';
|
|
95
95
|
|
|
96
|
+
// MCPL tool lifecycle (RFC-007) and tool classes (RFC-008): the types a host
|
|
97
|
+
// needs to write McplServerConfig.toolLifecycle policy and class tables, and
|
|
98
|
+
// the portable pattern matcher both use.
|
|
99
|
+
export { TOOL_CLASSES, DEFAULT_INPUT_CLASSES, globMatch } from './mcpl/index.js';
|
|
100
|
+
export type {
|
|
101
|
+
ToolClass,
|
|
102
|
+
ToolClassSource,
|
|
103
|
+
ToolLifecycleConfig,
|
|
104
|
+
ToolLifecycleNarrowing,
|
|
105
|
+
ToolLifecycleParams,
|
|
106
|
+
ToolObserveRule,
|
|
107
|
+
} from './mcpl/index.js';
|
|
108
|
+
|
|
96
109
|
// Per-channel conversation routing
|
|
97
110
|
export { ConversationRouter, DEFAULT_CLOSURE_PROMPT } from './mcpl/index.js';
|
|
98
111
|
export type {
|
|
@@ -48,6 +48,9 @@ const VOCABULARY: CapNode = {
|
|
|
48
48
|
modelInfo: null,
|
|
49
49
|
inferenceRequest: { streaming: null },
|
|
50
50
|
inferenceLifecycle: null,
|
|
51
|
+
// RFC-007: metadata observation and argument observation are separate
|
|
52
|
+
// leaves, separately granted (observation never bundles content).
|
|
53
|
+
toolLifecycle: { observe: null, inputs: null },
|
|
51
54
|
contextHooks: {
|
|
52
55
|
beforeInference: {
|
|
53
56
|
observe: null,
|
|
@@ -217,8 +220,36 @@ export function capabilityPatternMatches(pattern: string, path: string): boolean
|
|
|
217
220
|
// The grant
|
|
218
221
|
// ============================================================================
|
|
219
222
|
|
|
220
|
-
/** §13.4: denied unless explicitly enabled by host config.
|
|
221
|
-
|
|
223
|
+
/** §13.4: denied unless explicitly enabled by host config. RFC-007 adds both
|
|
224
|
+
* tool-lifecycle leaves: observing other servers' tool use (and, above all,
|
|
225
|
+
* their arguments) is an operator decision, never a default. */
|
|
226
|
+
const DENY_BY_DEFAULT: readonly string[] = [
|
|
227
|
+
'contextHooks.beforeInference.inject.system',
|
|
228
|
+
'toolLifecycle.observe',
|
|
229
|
+
'toolLifecycle.inputs',
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* RFC-007: stating a `toolLifecycle.observe` / `.inputs` policy block in the
|
|
234
|
+
* server's config is itself the explicit grant of that path — the operator
|
|
235
|
+
* who wrote a narrowing meant to grant what it narrows.
|
|
236
|
+
*/
|
|
237
|
+
function configGrants(config: Pick<McplServerConfig, 'toolLifecycle'>, path: string): boolean {
|
|
238
|
+
const key = path === 'toolLifecycle.observe' ? 'observe' : path === 'toolLifecycle.inputs' ? 'inputs' : null;
|
|
239
|
+
if (!key) return false;
|
|
240
|
+
const block = (config.toolLifecycle as Record<string, unknown> | undefined)?.[key];
|
|
241
|
+
if (block === undefined) return false;
|
|
242
|
+
// Only a policy OBJECT is a grant. `observe: false` (or null, or a string)
|
|
243
|
+
// must never read as "granted with no narrowing".
|
|
244
|
+
if (!isPlainObject(block)) {
|
|
245
|
+
console.error(
|
|
246
|
+
`[mcpl] toolLifecycle.${key} is ${JSON.stringify(block)}, not a policy object — ` +
|
|
247
|
+
`not granted by this block (RFC-007 §4.1)`,
|
|
248
|
+
);
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
222
253
|
|
|
223
254
|
export class CapabilityGrant {
|
|
224
255
|
/** Exact granted paths (already expanded — no patterns stored). */
|
|
@@ -286,7 +317,7 @@ export class CapabilityGrant {
|
|
|
286
317
|
*/
|
|
287
318
|
export function computeGrant(
|
|
288
319
|
capabilities: McplCapabilities | null,
|
|
289
|
-
config: Pick<McplServerConfig, 'enabledCapabilities'>,
|
|
320
|
+
config: Pick<McplServerConfig, 'enabledCapabilities' | 'toolLifecycle'>,
|
|
290
321
|
opts?: {
|
|
291
322
|
/** §5.1: `tools` is sourced from the OUTER standard MCP
|
|
292
323
|
* `capabilities.tools` — pass its presence here. The experimental
|
|
@@ -304,16 +335,33 @@ export function computeGrant(
|
|
|
304
335
|
!!config.enabledCapabilities?.some((pat) => capabilityPatternMatches(pat, path));
|
|
305
336
|
|
|
306
337
|
for (const path of advertised) {
|
|
307
|
-
if (DENY_BY_DEFAULT.includes(path) && !explicitlyEnabled(path)) {
|
|
338
|
+
if (DENY_BY_DEFAULT.includes(path) && !explicitlyEnabled(path) && !configGrants(config, path)) {
|
|
308
339
|
denied.push(path);
|
|
309
340
|
console.error(
|
|
310
|
-
`[mcpl] "${path}" advertised but DENIED BY DEFAULT (SPEC §13.4) — ` +
|
|
311
|
-
`grant it explicitly via enabledCapabilities if intended`,
|
|
341
|
+
`[mcpl] "${path}" advertised but DENIED BY DEFAULT (${path.startsWith('toolLifecycle.') ? 'RFC-007' : 'SPEC §13.4'}) — ` +
|
|
342
|
+
`grant it explicitly via enabledCapabilities${path.startsWith('toolLifecycle.') ? ' or a toolLifecycle policy block' : ''} if intended`,
|
|
312
343
|
);
|
|
313
344
|
continue;
|
|
314
345
|
}
|
|
315
346
|
granted.add(path);
|
|
316
347
|
}
|
|
317
348
|
|
|
349
|
+
// RFC-007 §4.1/§4.3 diagnostics — loud, because each is a grant that
|
|
350
|
+
// silently delivers less than the operator probably meant.
|
|
351
|
+
if (granted.has('toolLifecycle.inputs')) {
|
|
352
|
+
if (!granted.has('toolLifecycle.observe')) {
|
|
353
|
+
console.error('[mcpl] toolLifecycle.inputs granted without toolLifecycle.observe — delivers nothing (RFC-007 §4.1)');
|
|
354
|
+
}
|
|
355
|
+
const n = config.toolLifecycle?.inputs;
|
|
356
|
+
const hasTerm = isPlainObject(n) && ((Array.isArray(n.tools) && n.tools.length > 0)
|
|
357
|
+
|| n.classes === 'default' || (Array.isArray(n.classes) && n.classes.length > 0));
|
|
358
|
+
if (!hasTerm) {
|
|
359
|
+
console.error(
|
|
360
|
+
'[mcpl] toolLifecycle.inputs granted with no tools/classes narrowing — treated as narrowed to NO tools; ' +
|
|
361
|
+
'inputs is never unconditional (RFC-007 §4.3)',
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
318
366
|
return new CapabilityGrant(granted, denied.sort());
|
|
319
367
|
}
|
|
@@ -26,6 +26,7 @@ import type {
|
|
|
26
26
|
ChannelsIncomingParams,
|
|
27
27
|
ChannelsIncomingResult,
|
|
28
28
|
ChannelIncomingMessageResult,
|
|
29
|
+
ChannelIncomingMessage,
|
|
29
30
|
ChannelsPublishParams,
|
|
30
31
|
ChannelsOpenResult,
|
|
31
32
|
ChannelHistoryRequest,
|
|
@@ -36,6 +37,7 @@ import type { McplServerRegistry } from './server-registry.js';
|
|
|
36
37
|
import type { FeatureSetManager } from './feature-set-manager.js';
|
|
37
38
|
import type { ToolDefinition, ToolResult, ProcessEvent } from '../types/index.js';
|
|
38
39
|
import { expandCoreTags } from './tags.js';
|
|
40
|
+
import { validateCoalescedContent } from './push-coalescer.js';
|
|
39
41
|
import { CapabilityGrant } from './capability-grant.js';
|
|
40
42
|
|
|
41
43
|
// ============================================================================
|
|
@@ -401,6 +403,22 @@ const CHANNEL_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
401
403
|
required: [],
|
|
402
404
|
},
|
|
403
405
|
},
|
|
406
|
+
{
|
|
407
|
+
name: 'journal',
|
|
408
|
+
description:
|
|
409
|
+
'Write an entry in your private journal. The entry stays in your own context and ' +
|
|
410
|
+
'memory and is NOT sent to any channel, surface or person. Use it for anything longer ' +
|
|
411
|
+
'than a line that you want to keep for yourself — reflections, what you decided and ' +
|
|
412
|
+
'why, notes for later. It does not end your turn and does not affect where ordinary ' +
|
|
413
|
+
'text is routed; to end the turn without replying, call skip_reply afterwards.',
|
|
414
|
+
inputSchema: {
|
|
415
|
+
type: 'object' as const,
|
|
416
|
+
properties: {
|
|
417
|
+
content: { type: 'string', description: 'The journal entry (private; not sent anywhere).' },
|
|
418
|
+
},
|
|
419
|
+
required: ['content'],
|
|
420
|
+
},
|
|
421
|
+
},
|
|
404
422
|
{
|
|
405
423
|
name: 'skip_reply',
|
|
406
424
|
description:
|
|
@@ -414,7 +432,9 @@ const CHANNEL_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
414
432
|
properties: {
|
|
415
433
|
reason: {
|
|
416
434
|
type: 'string',
|
|
417
|
-
description:
|
|
435
|
+
description:
|
|
436
|
+
'Optional ONE short line on why you are not replying (private; not sent anywhere). ' +
|
|
437
|
+
'Keep it under ~100 characters — put anything longer in journal() first.',
|
|
418
438
|
},
|
|
419
439
|
wake_in_seconds: {
|
|
420
440
|
type: 'number',
|
|
@@ -434,6 +454,17 @@ const CHANNEL_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
434
454
|
// ============================================================================
|
|
435
455
|
|
|
436
456
|
interface ChannelRegistryOptions {
|
|
457
|
+
/**
|
|
458
|
+
* RFC-006: an admitted `channels/incoming` message carrying `coalesce`, with
|
|
459
|
+
* the event the ordinary path would have queued. The handler decides
|
|
460
|
+
* replace / append / withdraw and returns the per-message result. Throws a
|
|
461
|
+
* `CoalesceError` for a malformed or unauthorized occurrence.
|
|
462
|
+
*/
|
|
463
|
+
handleCoalescedIncoming?: (
|
|
464
|
+
serverId: string,
|
|
465
|
+
message: ChannelIncomingMessage,
|
|
466
|
+
event: McplChannelIncomingEvent,
|
|
467
|
+
) => Promise<ChannelIncomingMessageResult>;
|
|
437
468
|
/** Chronicle store used for durable desired channel lifecycle state. */
|
|
438
469
|
store?: JsStore;
|
|
439
470
|
/** Callback to determine whether an incoming message should trigger inference. */
|
|
@@ -625,6 +656,7 @@ export class ChannelRegistry {
|
|
|
625
656
|
/** One-time migration inputs from the retired recipe auto-open policy. */
|
|
626
657
|
private legacyPolicies = new Map<string, 'auto' | 'manual' | string[]>();
|
|
627
658
|
private migratedLegacyPolicies = new Set<string>();
|
|
659
|
+
private handleCoalescedIncoming?: ChannelRegistryOptions['handleCoalescedIncoming'];
|
|
628
660
|
|
|
629
661
|
constructor(
|
|
630
662
|
serverRegistry: McplServerRegistry,
|
|
@@ -640,6 +672,7 @@ export class ChannelRegistry {
|
|
|
640
672
|
) => void;
|
|
641
673
|
},
|
|
642
674
|
) {
|
|
675
|
+
this.handleCoalescedIncoming = options?.handleCoalescedIncoming;
|
|
643
676
|
this.serverRegistry = serverRegistry;
|
|
644
677
|
this.featureSetManager = featureSetManager;
|
|
645
678
|
this.pushEventFn = pushEventFn;
|
|
@@ -773,7 +806,8 @@ export class ChannelRegistry {
|
|
|
773
806
|
}
|
|
774
807
|
}
|
|
775
808
|
|
|
776
|
-
// Process added channels (validate per-descriptor, register
|
|
809
|
+
// Process added channels (validate per-descriptor, register; reconcile
|
|
810
|
+
// after responding, below)
|
|
777
811
|
const accepted: typeof params.added = [];
|
|
778
812
|
if (params.added) {
|
|
779
813
|
for (const channel of params.added) {
|
|
@@ -792,10 +826,18 @@ export class ChannelRegistry {
|
|
|
792
826
|
addedResults.push({ id: channel.id, accepted: true });
|
|
793
827
|
accepted.push(channel);
|
|
794
828
|
}
|
|
795
|
-
if (accepted.length > 0) await this.reconcileChannels(serverId, accepted);
|
|
796
829
|
}
|
|
830
|
+
|
|
831
|
+
// Respond before reconciliation, as handleRegister does. A server that
|
|
832
|
+
// announces from inside a request it is serving (a tool that refreshes
|
|
833
|
+
// or subscribes) cannot read the channels/open or channels/close that
|
|
834
|
+
// reconciling sends until this response arrives; reconciling first
|
|
835
|
+
// deadlocks both sides until one times out (#160). The verdicts are
|
|
836
|
+
// settled above, so nothing in the response depends on reconciling.
|
|
797
837
|
responder?.respond({ results: addedResults });
|
|
798
838
|
|
|
839
|
+
if (accepted.length > 0) await this.reconcileChannels(serverId, accepted);
|
|
840
|
+
|
|
799
841
|
this.emitTraceFn({
|
|
800
842
|
type: 'mcpl:channels-changed',
|
|
801
843
|
serverId,
|
|
@@ -811,14 +853,23 @@ export class ChannelRegistry {
|
|
|
811
853
|
* Converts each message's content, pushes McplChannelIncomingEvent to the
|
|
812
854
|
* queue, and responds with per-message results.
|
|
813
855
|
*/
|
|
814
|
-
handleIncoming(
|
|
856
|
+
async handleIncoming(
|
|
815
857
|
serverId: string,
|
|
816
858
|
params: ChannelsIncomingParams,
|
|
817
859
|
responder?: Responder,
|
|
818
|
-
): void {
|
|
860
|
+
): Promise<void> {
|
|
819
861
|
const results: ChannelIncomingMessageResult[] = [];
|
|
820
862
|
|
|
821
863
|
for (const message of params.messages) {
|
|
864
|
+
if (!message || typeof message.channelId !== 'string' || !message.channelId
|
|
865
|
+
|| typeof message.messageId !== 'string' || !message.messageId) {
|
|
866
|
+
results.push({
|
|
867
|
+
messageId: typeof message?.messageId === 'string' ? message.messageId : '',
|
|
868
|
+
accepted: false,
|
|
869
|
+
reason: message?.coalesce !== undefined ? 'coalesce_invalid' : 'invalid channel/message identity',
|
|
870
|
+
});
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
822
873
|
// §14.5 FIRST, before ANY semantic processing: admission against the
|
|
823
874
|
// actually-registered channel precedes tag expansion and content
|
|
824
875
|
// conversion — decoding an unregistered sender's payload (including
|
|
@@ -863,6 +914,17 @@ export class ChannelRegistry {
|
|
|
863
914
|
|
|
864
915
|
// ACCEPTED from here down: semantic processing only for admitted
|
|
865
916
|
// messages. §16.3 core-tag closure, then content conversion.
|
|
917
|
+
const coalesced = message.coalesce !== undefined && !!this.handleCoalescedIncoming;
|
|
918
|
+
if (coalesced) {
|
|
919
|
+
// RFC-006 §13: malformed content on a coalesced item is that item's
|
|
920
|
+
// failure, not the batch's — check the shape before converting.
|
|
921
|
+
try {
|
|
922
|
+
validateCoalescedContent(message.content);
|
|
923
|
+
} catch (error) {
|
|
924
|
+
results.push({ messageId: message.messageId, accepted: false, reason: 'coalesce_invalid' });
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
866
928
|
if (message.tags) message.tags = expandCoreTags(message.tags);
|
|
867
929
|
const convertedContent: ContentBlock[] = message.content.map(convertBlock);
|
|
868
930
|
|
|
@@ -870,16 +932,19 @@ export class ChannelRegistry {
|
|
|
870
932
|
// deliberately after §14.5 validation: a rejected message from an
|
|
871
933
|
// unregistered channel must not retarget outbound speech (the locus is
|
|
872
934
|
// exactly the authority a self-attested channel would be stealing).
|
|
873
|
-
|
|
874
|
-
this
|
|
875
|
-
|
|
876
|
-
|
|
935
|
+
// A coalesced item is "accepted" only once the coalescer admits it, so
|
|
936
|
+
// for those this runs after the hook (below).
|
|
937
|
+
const markAccepted = () => {
|
|
938
|
+
this.defaultPublishChannel = message.channelId;
|
|
939
|
+
this.defaultPublishMessageId = message.messageId;
|
|
940
|
+
this.defaultPublishThreadId = message.threadId;
|
|
877
941
|
// A server sending channels/incoming is authoritative evidence that
|
|
878
942
|
// the transport is actually open. This repairs transient status only;
|
|
879
943
|
// durable desired state still changes exclusively through lifecycle
|
|
880
944
|
// operations.
|
|
881
945
|
this.channels.get(incomingKey)!.open = true;
|
|
882
|
-
}
|
|
946
|
+
};
|
|
947
|
+
if (!coalesced) markAccepted();
|
|
883
948
|
|
|
884
949
|
// Determine whether to trigger inference
|
|
885
950
|
let triggerInference = true;
|
|
@@ -918,6 +983,26 @@ export class ChannelRegistry {
|
|
|
918
983
|
triggerInference,
|
|
919
984
|
};
|
|
920
985
|
|
|
986
|
+
if (coalesced) {
|
|
987
|
+
// RFC-006 §14.3: a coalesced message is admitted like any other and
|
|
988
|
+
// then handed, with the event the ordinary path would have queued, to
|
|
989
|
+
// the coalescer, which replaces, appends or withdraws. Malformed
|
|
990
|
+
// `coalesce` is a per-message failure; siblings are unaffected.
|
|
991
|
+
try {
|
|
992
|
+
const result = await this.handleCoalescedIncoming!(serverId, message, event);
|
|
993
|
+
if (result.accepted) markAccepted();
|
|
994
|
+
results.push(result);
|
|
995
|
+
} catch (error) {
|
|
996
|
+
const err = error as Error & { code?: number };
|
|
997
|
+
results.push({
|
|
998
|
+
messageId: message.messageId,
|
|
999
|
+
accepted: false,
|
|
1000
|
+
reason: err.code === -32602 ? 'coalesce_invalid' : err.message,
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
921
1006
|
// Push to the processing queue
|
|
922
1007
|
// Cast through unknown because McplChannelIncomingEvent matches the
|
|
923
1008
|
// CustomEvent `${string}:${string}` type pattern but lacks an index signature.
|
|
@@ -1128,6 +1213,18 @@ export class ChannelRegistry {
|
|
|
1128
1213
|
* Get the descriptor for a channel by its channelId (first match across
|
|
1129
1214
|
* servers). Used by the conversation router for DM classification.
|
|
1130
1215
|
*/
|
|
1216
|
+
/**
|
|
1217
|
+
* RFC-006 §3.2: is `channelId` registered BY `serverId` through a server
|
|
1218
|
+
* declaration (channels/register, channels/changed) or an authorized open?
|
|
1219
|
+
* A placeholder minted from a push's `origin` (ensureChannelRegistered)
|
|
1220
|
+
* does not count: a channel id appearing in an untrusted field is not a
|
|
1221
|
+
* registration.
|
|
1222
|
+
*/
|
|
1223
|
+
isDeclaredChannel(serverId: string, channelId: string): boolean {
|
|
1224
|
+
const entry = this.channels.get(`${serverId}:${channelId}`);
|
|
1225
|
+
return !!entry && !(entry.descriptor.metadata as { lazyRegistered?: boolean } | undefined)?.lazyRegistered;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1131
1228
|
getDescriptor(channelId: string): ChannelDescriptor | undefined {
|
|
1132
1229
|
return this.findChannelEntry(channelId)?.descriptor;
|
|
1133
1230
|
}
|
|
@@ -1218,6 +1315,9 @@ export class ChannelRegistry {
|
|
|
1218
1315
|
case 'think':
|
|
1219
1316
|
return this.handleToolThink(input as { content?: string });
|
|
1220
1317
|
|
|
1318
|
+
case 'journal':
|
|
1319
|
+
return this.handleToolJournal(input as { content?: string });
|
|
1320
|
+
|
|
1221
1321
|
case 'skip_reply':
|
|
1222
1322
|
return this.handleToolSkipReply(input as { reason?: string; wake_in_seconds?: number });
|
|
1223
1323
|
|
|
@@ -2345,6 +2445,44 @@ export class ChannelRegistry {
|
|
|
2345
2445
|
return { error: `no channel matches "${trimmed}"`, ...(near.length ? { candidates: near } : {}) };
|
|
2346
2446
|
}
|
|
2347
2447
|
|
|
2448
|
+
/**
|
|
2449
|
+
* The `>>` target to show an agent for a channel: one whitespace-free token
|
|
2450
|
+
* that `resolveProseTarget()` maps back to this same channel. The prefix
|
|
2451
|
+
* grammar takes the target as the first non-whitespace run, so a label with
|
|
2452
|
+
* a space can't be quoted verbatim: `>>#DM: alice` parses as target `#DM:`
|
|
2453
|
+
* plus body `alice …`, and `>>#fable (antra's server)` delivers
|
|
2454
|
+
* `(antra's server)` as text. Tried in order: `@name` for a DM, `#label`,
|
|
2455
|
+
* `#name` (label without its server suffix), the descriptor id.
|
|
2456
|
+
*
|
|
2457
|
+
* Undefined when there is no safe token: the channel isn't registered (on
|
|
2458
|
+
* `serverId`, when given), no candidate is whitespace-free and resolves back,
|
|
2459
|
+
* or the id is registered by more than one server. A resolved target names a
|
|
2460
|
+
* channel by id alone, and ids are unique only within a connection, so a
|
|
2461
|
+
* shared id could route the reply through the wrong server.
|
|
2462
|
+
*/
|
|
2463
|
+
proseTargetFor(channelId: string, serverId?: string): string | undefined {
|
|
2464
|
+
const sameId = [...this.channels.values()].filter((e) => e.descriptor.id === channelId);
|
|
2465
|
+
const entry = serverId ? sameId.find((e) => e.serverId === serverId) : sameId[0];
|
|
2466
|
+
if (!entry || sameId.length > 1) return undefined;
|
|
2467
|
+
const d = entry.descriptor;
|
|
2468
|
+
const label = d.label ?? '';
|
|
2469
|
+
const meta = d.metadata as { channelType?: string; recipientName?: string } | undefined;
|
|
2470
|
+
const isDm = meta?.channelType === 'dm' || label.toLowerCase().startsWith('dm: ') || d.id.includes(':dm:');
|
|
2471
|
+
const dmName = meta?.recipientName ?? (label.toLowerCase().startsWith('dm: ') ? label.slice(4) : undefined);
|
|
2472
|
+
const bare = label.replace(/^#/, '');
|
|
2473
|
+
const candidates = [
|
|
2474
|
+
...(isDm && dmName ? [`@${dmName}`] : []),
|
|
2475
|
+
...(bare ? [`#${bare}`, `#${bare.replace(/\s*\([^)]*\)\s*$/, '')}`] : []),
|
|
2476
|
+
d.id,
|
|
2477
|
+
];
|
|
2478
|
+
for (const c of candidates) {
|
|
2479
|
+
if (/\s/.test(c) || c === '#') continue;
|
|
2480
|
+
const r = this.resolveProseTarget(c);
|
|
2481
|
+
if ('channelId' in r && r.channelId === d.id) return c;
|
|
2482
|
+
}
|
|
2483
|
+
return undefined;
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2348
2486
|
/**
|
|
2349
2487
|
* Open a channel because something was DELIVERED into it (explicit send
|
|
2350
2488
|
* tool or routed speech). Sending into a closed channel is not a thing:
|
|
@@ -2670,6 +2808,28 @@ export class ChannelRegistry {
|
|
|
2670
2808
|
};
|
|
2671
2809
|
}
|
|
2672
2810
|
|
|
2811
|
+
/**
|
|
2812
|
+
* Handle the synthesized `journal` tool — a private place for long-form
|
|
2813
|
+
* notes. Sends nothing, does not end the turn, does not touch prose routing.
|
|
2814
|
+
*
|
|
2815
|
+
* Why it exists (sill, 2026-09-19): residents were keeping 2–3KB diaries in
|
|
2816
|
+
* `skip_reply.reason`. Long prose in a private-REASONING tool argument
|
|
2817
|
+
* (`skip_reply.reason`, `think.content`) makes replayed history read as a
|
|
2818
|
+
* reasoning trace, and every memory-compression request over it is refused
|
|
2819
|
+
* `reasoning_extraction` regardless of content; the same prose in a
|
|
2820
|
+
* note-taking tool passes (canary record: context-manager
|
|
2821
|
+
* `tool-prose-hoist.ts`, whose fallback rung rewrites old history into calls
|
|
2822
|
+
* to THIS tool — so the result wording below is mirrored there as
|
|
2823
|
+
* DEFAULT_TOOL_PROSE_RESULT; keep the two in step).
|
|
2824
|
+
*/
|
|
2825
|
+
private handleToolJournal(_input: { content?: string }): ToolResult {
|
|
2826
|
+
return {
|
|
2827
|
+
success: true,
|
|
2828
|
+
// No echo: the entry is already in the tool_use block.
|
|
2829
|
+
data: { recorded: true, note: 'Journal entry recorded (private — not sent anywhere).' },
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2673
2833
|
/**
|
|
2674
2834
|
* Handle the synthesized `skip_reply` tool — the deliberate "stay silent"
|
|
2675
2835
|
* signal. A no-op as far as any surface is concerned (sends nothing); its
|
package/src/mcpl/index.ts
CHANGED
|
@@ -118,6 +118,35 @@ export { ScopeManager, type ElevationHandler } from './scope-manager.js';
|
|
|
118
118
|
// Hook orchestration (beforeInference/afterInference fan-out)
|
|
119
119
|
export { HookOrchestrator } from './hook-orchestrator.js';
|
|
120
120
|
|
|
121
|
+
export {
|
|
122
|
+
ToolLifecycleEmitter,
|
|
123
|
+
parseToolObserveParams,
|
|
124
|
+
selectFields,
|
|
125
|
+
boundInput,
|
|
126
|
+
openingFor,
|
|
127
|
+
DEFAULT_MAX_INPUT_BYTES,
|
|
128
|
+
TOOL_OBSERVE_LIMITS,
|
|
129
|
+
type ToolPhase,
|
|
130
|
+
type ToolLifecycleParams,
|
|
131
|
+
type ToolObserveRule,
|
|
132
|
+
type ToolObserveMatch,
|
|
133
|
+
type ToolLifecycleConfig,
|
|
134
|
+
type ToolLifecycleNarrowing,
|
|
135
|
+
} from './tool-lifecycle.js';
|
|
136
|
+
export {
|
|
137
|
+
TOOL_CLASSES,
|
|
138
|
+
TOOL_CLASS_META_KEY,
|
|
139
|
+
DEFAULT_INPUT_CLASSES,
|
|
140
|
+
BUILTIN_TOOL_CLASSES,
|
|
141
|
+
parseDeclaredClasses,
|
|
142
|
+
resolveToolClass,
|
|
143
|
+
isToolClass,
|
|
144
|
+
type ToolClass,
|
|
145
|
+
type ToolClassSource,
|
|
146
|
+
type EffectiveToolClass,
|
|
147
|
+
} from './tool-classes.js';
|
|
148
|
+
export { globMatch } from './tool-glob.js';
|
|
149
|
+
|
|
121
150
|
// Push events (Section 9)
|
|
122
151
|
export { PushHandler } from './push-handler.js';
|
|
123
152
|
export type { McplPushEvent } from './push-handler.js';
|