@animalabs/agent-framework 0.8.0 → 0.10.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/dist/src/agent.d.ts +11 -1
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +33 -3
- package/dist/src/agent.js.map +1 -1
- package/dist/src/framework.d.ts +184 -18
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1323 -246
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +73 -7
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +399 -22
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/types.d.ts +56 -0
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/capability-grant.d.ts +115 -0
- package/dist/src/mcpl/capability-grant.d.ts.map +1 -0
- package/dist/src/mcpl/capability-grant.js +291 -0
- package/dist/src/mcpl/capability-grant.js.map +1 -0
- package/dist/src/mcpl/capability-mask.d.ts +51 -0
- package/dist/src/mcpl/capability-mask.d.ts.map +1 -0
- package/dist/src/mcpl/capability-mask.js +190 -0
- package/dist/src/mcpl/capability-mask.js.map +1 -0
- package/dist/src/mcpl/channel-registry.d.ts +3 -1
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +163 -36
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.d.ts +8 -3
- package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.js +28 -8
- package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
- package/dist/src/mcpl/errors.d.ts +6 -0
- package/dist/src/mcpl/errors.d.ts.map +1 -1
- package/dist/src/mcpl/errors.js +6 -0
- package/dist/src/mcpl/errors.js.map +1 -1
- package/dist/src/mcpl/feature-set-manager.d.ts +10 -1
- package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
- package/dist/src/mcpl/feature-set-manager.js +45 -4
- package/dist/src/mcpl/feature-set-manager.js.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts +10 -14
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.js +58 -67
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
- package/dist/src/mcpl/index.d.ts +1 -1
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/prose-grammar.d.ts +11 -0
- package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
- package/dist/src/mcpl/prose-grammar.js +21 -0
- package/dist/src/mcpl/prose-grammar.js.map +1 -1
- package/dist/src/mcpl/prose-stream-router.d.ts +11 -2
- package/dist/src/mcpl/prose-stream-router.d.ts.map +1 -1
- package/dist/src/mcpl/prose-stream-router.js +73 -13
- package/dist/src/mcpl/prose-stream-router.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +20 -2
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +93 -9
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +231 -32
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/server-registry.d.ts.map +1 -1
- package/dist/src/mcpl/server-registry.js +4 -3
- package/dist/src/mcpl/server-registry.js.map +1 -1
- package/dist/src/mcpl/tags.d.ts +24 -0
- package/dist/src/mcpl/tags.d.ts.map +1 -0
- package/dist/src/mcpl/tags.js +52 -0
- package/dist/src/mcpl/tags.js.map +1 -0
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +10 -0
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +159 -64
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js +16 -2
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +23 -0
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +48 -0
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/sync.d.ts +11 -2
- package/dist/src/modules/workspace/sync.d.ts.map +1 -1
- package/dist/src/modules/workspace/sync.js +18 -8
- package/dist/src/modules/workspace/sync.js.map +1 -1
- package/dist/src/refusal-reactions.d.ts +27 -0
- package/dist/src/refusal-reactions.d.ts.map +1 -0
- package/dist/src/refusal-reactions.js +35 -0
- package/dist/src/refusal-reactions.js.map +1 -0
- package/dist/src/tool-result-history.d.ts +20 -0
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +26 -5
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/types/agent.d.ts +39 -1
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +16 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +24 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/channel-registry-routing.test.js +38 -10
- package/dist/test/channel-registry-routing.test.js.map +1 -1
- package/dist/test/channel-registry-typing.test.js +10 -1
- package/dist/test/channel-registry-typing.test.js.map +1 -1
- package/dist/test/checkpoint-attribution.test.js +168 -8
- package/dist/test/checkpoint-attribution.test.js.map +1 -1
- package/dist/test/conversation-fork-injection-scoping.test.js +8 -3
- package/dist/test/conversation-fork-injection-scoping.test.js.map +1 -1
- package/dist/test/event-gate-passthrough.test.d.ts +15 -0
- package/dist/test/event-gate-passthrough.test.d.ts.map +1 -0
- package/dist/test/event-gate-passthrough.test.js +289 -0
- package/dist/test/event-gate-passthrough.test.js.map +1 -0
- package/dist/test/event-gate-selfwake.test.d.ts +15 -0
- package/dist/test/event-gate-selfwake.test.d.ts.map +1 -0
- package/dist/test/event-gate-selfwake.test.js +125 -0
- package/dist/test/event-gate-selfwake.test.js.map +1 -0
- package/dist/test/explicit-prose-routing.test.js +137 -7
- package/dist/test/explicit-prose-routing.test.js.map +1 -1
- package/dist/test/fixtures/awareness-barrier-mcpl-server.mjs +276 -0
- package/dist/test/fixtures/flaky-mcpl-server.mjs +41 -0
- package/dist/test/machine-close-provenance.test.js +6 -0
- package/dist/test/machine-close-provenance.test.js.map +1 -1
- package/dist/test/mcpl-awareness-barrier.test.js +10 -0
- package/dist/test/mcpl-awareness-barrier.test.js.map +1 -1
- package/dist/test/mcpl-capability-grant.test.d.ts +10 -0
- package/dist/test/mcpl-capability-grant.test.d.ts.map +1 -0
- package/dist/test/mcpl-capability-grant.test.js +109 -0
- package/dist/test/mcpl-capability-grant.test.js.map +1 -0
- package/dist/test/mcpl-capability-scoping.test.d.ts +2 -0
- package/dist/test/mcpl-capability-scoping.test.d.ts.map +1 -0
- package/dist/test/mcpl-capability-scoping.test.js +266 -0
- package/dist/test/mcpl-capability-scoping.test.js.map +1 -0
- package/dist/test/mcpl-gate-roundtrip.test.js +13 -0
- package/dist/test/mcpl-gate-roundtrip.test.js.map +1 -1
- package/dist/test/mcpl-list-status.test.d.ts +2 -0
- package/dist/test/mcpl-list-status.test.d.ts.map +1 -0
- package/dist/test/mcpl-list-status.test.js +78 -0
- package/dist/test/mcpl-list-status.test.js.map +1 -0
- package/dist/test/mcpl-manifest-changed.test.d.ts +2 -0
- package/dist/test/mcpl-manifest-changed.test.d.ts.map +1 -0
- package/dist/test/mcpl-manifest-changed.test.js +177 -0
- package/dist/test/mcpl-manifest-changed.test.js.map +1 -0
- package/dist/test/mcpl-reconnect-events.test.js +27 -2
- package/dist/test/mcpl-reconnect-events.test.js.map +1 -1
- package/dist/test/mcpl-reconnect-reregistration.test.js +53 -17
- package/dist/test/mcpl-reconnect-reregistration.test.js.map +1 -1
- package/dist/test/mcpl-request-timeout.test.js +1 -1
- package/dist/test/mcpl-request-timeout.test.js.map +1 -1
- package/dist/test/physical-window-restart.test.d.ts +14 -0
- package/dist/test/physical-window-restart.test.d.ts.map +1 -0
- package/dist/test/physical-window-restart.test.js +222 -0
- package/dist/test/physical-window-restart.test.js.map +1 -0
- package/dist/test/present-while-acting.test.js +199 -1
- package/dist/test/present-while-acting.test.js.map +1 -1
- package/dist/test/prose-stream-router.test.js +43 -0
- package/dist/test/prose-stream-router.test.js.map +1 -1
- package/dist/test/refusal-reaction-baseline.test.d.ts +2 -0
- package/dist/test/refusal-reaction-baseline.test.d.ts.map +1 -0
- package/dist/test/refusal-reaction-baseline.test.js +74 -0
- package/dist/test/refusal-reaction-baseline.test.js.map +1 -0
- package/dist/test/state-scaling-e2e.test.js +7 -1
- package/dist/test/state-scaling-e2e.test.js.map +1 -1
- package/dist/test/state-scaling.test.js +9 -1
- package/dist/test/state-scaling.test.js.map +1 -1
- package/dist/test/tool-result-spill.test.d.ts +13 -0
- package/dist/test/tool-result-spill.test.d.ts.map +1 -0
- package/dist/test/tool-result-spill.test.js +472 -0
- package/dist/test/tool-result-spill.test.js.map +1 -0
- package/dist/test/trunk-channel-routing.test.js +45 -0
- package/dist/test/trunk-channel-routing.test.js.map +1 -1
- package/dist/test/xml-round-persist.test.d.ts +19 -0
- package/dist/test/xml-round-persist.test.d.ts.map +1 -0
- package/dist/test/xml-round-persist.test.js +235 -0
- package/dist/test/xml-round-persist.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -4
- package/src/agent.ts +38 -4
- package/src/framework.ts +1488 -272
- package/src/gate/event-gate.ts +427 -24
- package/src/gate/types.ts +58 -0
- package/src/index.ts +1 -0
- package/src/mcpl/capability-grant.ts +319 -0
- package/src/mcpl/capability-mask.ts +228 -0
- package/src/mcpl/channel-registry.ts +167 -39
- package/src/mcpl/checkpoint-manager.ts +28 -8
- package/src/mcpl/errors.ts +7 -0
- package/src/mcpl/feature-set-manager.ts +50 -5
- package/src/mcpl/hook-orchestrator.ts +64 -86
- package/src/mcpl/index.ts +0 -2
- package/src/mcpl/prose-grammar.ts +21 -0
- package/src/mcpl/prose-stream-router.ts +65 -14
- package/src/mcpl/push-handler.ts +18 -2
- package/src/mcpl/server-connection.ts +262 -34
- package/src/mcpl/server-registry.ts +4 -3
- package/src/mcpl/tags.ts +50 -0
- package/src/mcpl/transport.ts +11 -0
- package/src/mcpl/types.ts +176 -78
- package/src/modules/workspace/index.ts +48 -0
- package/src/modules/workspace/sync.ts +30 -7
- package/src/refusal-reactions.ts +37 -0
- package/src/tool-result-history.ts +27 -5
- package/src/types/agent.ts +40 -1
- package/src/types/framework.ts +17 -0
- package/src/types/trace.ts +22 -0
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This is the bridge between the MCPL protocol and the agent-framework inference
|
|
5
5
|
* pipeline. It collects ContextInjection[] from MCPL servers via beforeInference
|
|
6
|
-
* and feeds them into context-manager's compile().
|
|
7
|
-
*
|
|
6
|
+
* and feeds them into context-manager's compile(). Turn boundaries are announced
|
|
7
|
+
* via metadata-only `inference/lifecycle` notifications (§10.5) —
|
|
8
|
+
* context/afterInference and its modifiedResponse are removed in 0.5.0.
|
|
8
9
|
*
|
|
9
10
|
* Design principles:
|
|
10
11
|
* - Fail-open: timeouts and errors never block inference
|
|
@@ -20,18 +21,24 @@ import type {
|
|
|
20
21
|
McplContextInjection,
|
|
21
22
|
BeforeInferenceParams,
|
|
22
23
|
BeforeInferenceResult,
|
|
23
|
-
|
|
24
|
-
AfterInferenceResult,
|
|
24
|
+
InferenceLifecycleParams,
|
|
25
25
|
} from './types.js';
|
|
26
26
|
import type { McplServerRegistry } from './server-registry.js';
|
|
27
27
|
import type { McplServerConnection } from './server-connection.js';
|
|
28
28
|
import type { FeatureSetManager } from './feature-set-manager.js';
|
|
29
|
+
import { CapabilityGrant } from './capability-grant.js';
|
|
29
30
|
|
|
30
31
|
/** Timeout for beforeInference per server (fail-open). */
|
|
31
32
|
const BEFORE_INFERENCE_TIMEOUT_MS = 5_000;
|
|
32
33
|
|
|
33
|
-
/**
|
|
34
|
-
|
|
34
|
+
/** The three injection positions and their §6.2 capability paths. Position
|
|
35
|
+
* is the TYPED field on the injection — authorization never reads the
|
|
36
|
+
* response's featureSet (§5.4). */
|
|
37
|
+
const INJECT_LEAVES: ReadonlyArray<{ position: string; path: string }> = [
|
|
38
|
+
{ position: 'system', path: 'contextHooks.beforeInference.inject.system' },
|
|
39
|
+
{ position: 'beforeUser', path: 'contextHooks.beforeInference.inject.beforeUser' },
|
|
40
|
+
{ position: 'afterUser', path: 'contextHooks.beforeInference.inject.afterUser' },
|
|
41
|
+
];
|
|
35
42
|
|
|
36
43
|
/**
|
|
37
44
|
* Races a promise against a timeout. Rejects with a descriptive error on timeout.
|
|
@@ -141,7 +148,16 @@ export class HookOrchestrator {
|
|
|
141
148
|
* Fail-open: servers that time out or error are silently skipped.
|
|
142
149
|
*/
|
|
143
150
|
async beforeInference(params: BeforeInferenceParams): Promise<ContextInjection[]> {
|
|
144
|
-
|
|
151
|
+
// §5.4: the GRANT decides fan-out, not the raw advertisement — which
|
|
152
|
+
// also fixes selection for the recursive §5.1 shape, where
|
|
153
|
+
// `beforeInference: {observe: true, …}` is an object, not `true`. A
|
|
154
|
+
// server granted any inject.* leaf but not observe is still CALLED
|
|
155
|
+
// (§10.1 — the hook is how injection happens) and receives
|
|
156
|
+
// `userMessage: null` instead of the user's text.
|
|
157
|
+
const servers = this.registry.getAllServers().filter((s) =>
|
|
158
|
+
CapabilityGrant.of(s).has('contextHooks.beforeInference.observe')
|
|
159
|
+
|| INJECT_LEAVES.some((leaf) => CapabilityGrant.of(s).has(leaf.path)),
|
|
160
|
+
);
|
|
145
161
|
if (servers.length === 0) {
|
|
146
162
|
return [];
|
|
147
163
|
}
|
|
@@ -155,23 +171,20 @@ export class HookOrchestrator {
|
|
|
155
171
|
}
|
|
156
172
|
|
|
157
173
|
/**
|
|
158
|
-
* Fan out `
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
174
|
+
* Fan out `inference/lifecycle` (§10.5) — the metadata-only replacement
|
|
175
|
+
* for context/afterInference. Notifications, fire-and-forget, gated on the
|
|
176
|
+
* inferenceLifecycle grant. BEST-EFFORT by design: the host attempts one
|
|
177
|
+
* terminal per `started` on every exit path it controls; consumers dedupe
|
|
178
|
+
* by inferenceId and keep a safety timeout. Never blocks the turn.
|
|
163
179
|
*/
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return await this.fanOutAfterInference(servers, params);
|
|
173
|
-
} finally {
|
|
174
|
-
this._isInHook = false;
|
|
180
|
+
emitLifecycle(params: InferenceLifecycleParams): void {
|
|
181
|
+
for (const server of this.registry.getAllServers()) {
|
|
182
|
+
if (!CapabilityGrant.of(server).has('inferenceLifecycle')) continue;
|
|
183
|
+
try {
|
|
184
|
+
server.sendInferenceLifecycle(params);
|
|
185
|
+
} catch {
|
|
186
|
+
/* best-effort — a failed notify never disturbs the turn */
|
|
187
|
+
}
|
|
175
188
|
}
|
|
176
189
|
}
|
|
177
190
|
|
|
@@ -184,13 +197,21 @@ export class HookOrchestrator {
|
|
|
184
197
|
params: BeforeInferenceParams,
|
|
185
198
|
): Promise<ContextInjection[]> {
|
|
186
199
|
const results = await Promise.allSettled(
|
|
187
|
-
servers.map((server) =>
|
|
188
|
-
|
|
189
|
-
|
|
200
|
+
servers.map((server) => {
|
|
201
|
+
// §10.1: a server not granted observe MUST receive
|
|
202
|
+
// `userMessage: null` — the field is ABSENT authority, not merely
|
|
203
|
+
// discouraged — while the hook is still invoked so granted
|
|
204
|
+
// injection positions keep working (write-without-read).
|
|
205
|
+
const perServer: BeforeInferenceParams =
|
|
206
|
+
CapabilityGrant.of(server).has('contextHooks.beforeInference.observe')
|
|
207
|
+
? params
|
|
208
|
+
: { ...params, userMessage: null };
|
|
209
|
+
return withTimeout(
|
|
210
|
+
server.sendBeforeInference(perServer),
|
|
190
211
|
BEFORE_INFERENCE_TIMEOUT_MS,
|
|
191
212
|
`beforeInference to "${server.id}"`,
|
|
192
|
-
).then((result) => ({ server, result }))
|
|
193
|
-
),
|
|
213
|
+
).then((result) => ({ server, result }));
|
|
214
|
+
}),
|
|
194
215
|
);
|
|
195
216
|
|
|
196
217
|
const injections: ContextInjection[] = [];
|
|
@@ -203,7 +224,9 @@ export class HookOrchestrator {
|
|
|
203
224
|
|
|
204
225
|
const { server, result } = settled.value;
|
|
205
226
|
|
|
206
|
-
//
|
|
227
|
+
// Feature-set discipline (diagnostics; §5.4 forbids using the
|
|
228
|
+
// response-supplied featureSet as AUTHORIZATION — that is the
|
|
229
|
+
// position check below).
|
|
207
230
|
try {
|
|
208
231
|
this.featureSetManager.validateInbound(server.id, result.featureSet);
|
|
209
232
|
} catch {
|
|
@@ -211,9 +234,20 @@ export class HookOrchestrator {
|
|
|
211
234
|
continue;
|
|
212
235
|
}
|
|
213
236
|
|
|
214
|
-
//
|
|
237
|
+
// §5.4/§10.8: authorize EACH injection by its typed position against
|
|
238
|
+
// the grant CURRENT NOW, at response receipt — not when the request
|
|
239
|
+
// was sent. A revocation that landed while the hook was in flight is
|
|
240
|
+
// enforced here without extra machinery.
|
|
215
241
|
if (result.contextInjections && result.contextInjections.length > 0) {
|
|
216
242
|
for (const mcplInj of result.contextInjections) {
|
|
243
|
+
const leaf = INJECT_LEAVES.find((l) => l.position === mcplInj.position);
|
|
244
|
+
if (!leaf || !CapabilityGrant.of(server).has(leaf.path)) {
|
|
245
|
+
console.error(
|
|
246
|
+
`[mcpl] ${server.id}: dropped beforeInference injection at position ` +
|
|
247
|
+
`"${mcplInj.position}" — not in the effective grant (§10.8)`,
|
|
248
|
+
);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
217
251
|
injections.push(convertMcplInjection(mcplInj));
|
|
218
252
|
}
|
|
219
253
|
}
|
|
@@ -222,60 +256,4 @@ export class HookOrchestrator {
|
|
|
222
256
|
return injections;
|
|
223
257
|
}
|
|
224
258
|
|
|
225
|
-
// ==========================================================================
|
|
226
|
-
// Private: afterInference fan-out
|
|
227
|
-
// ==========================================================================
|
|
228
|
-
|
|
229
|
-
private async fanOutAfterInference(
|
|
230
|
-
servers: McplServerConnection[],
|
|
231
|
-
params: AfterInferenceParams,
|
|
232
|
-
): Promise<AfterInferenceResult | null> {
|
|
233
|
-
const blockingPromises: Promise<AfterInferenceResult | null>[] = [];
|
|
234
|
-
|
|
235
|
-
for (const server of servers) {
|
|
236
|
-
const isBlocking = this.isBlockingAfterInference(server);
|
|
237
|
-
|
|
238
|
-
if (isBlocking) {
|
|
239
|
-
// Send as request, await response with timeout
|
|
240
|
-
const promise = withTimeout(
|
|
241
|
-
server.sendAfterInference(params, true) as Promise<AfterInferenceResult>,
|
|
242
|
-
AFTER_INFERENCE_BLOCKING_TIMEOUT_MS,
|
|
243
|
-
`afterInference (blocking) to "${server.id}"`,
|
|
244
|
-
).catch((): null => {
|
|
245
|
-
// Fail-open: timeout or error — treat as no modification
|
|
246
|
-
return null;
|
|
247
|
-
});
|
|
248
|
-
blockingPromises.push(promise);
|
|
249
|
-
} else {
|
|
250
|
-
// Send as notification (fire-and-forget)
|
|
251
|
-
server.sendAfterInference(params, false);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (blockingPromises.length === 0) {
|
|
256
|
-
return null;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Await all blocking responses and return the first valid result
|
|
260
|
-
const results = await Promise.all(blockingPromises);
|
|
261
|
-
for (const result of results) {
|
|
262
|
-
if (result && result.modifiedResponse) {
|
|
263
|
-
return result;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
return null;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Check if a server's afterInference capability is blocking.
|
|
272
|
-
*/
|
|
273
|
-
private isBlockingAfterInference(server: McplServerConnection): boolean {
|
|
274
|
-
const caps = server.capabilities;
|
|
275
|
-
if (!caps?.contextHooks?.afterInference) {
|
|
276
|
-
return false;
|
|
277
|
-
}
|
|
278
|
-
const afterCap = caps.contextHooks.afterInference;
|
|
279
|
-
return typeof afterCap === 'object' && afterCap !== null && 'blocking' in afterCap && afterCap.blocking === true;
|
|
280
|
-
}
|
|
281
259
|
}
|
package/src/mcpl/index.ts
CHANGED
|
@@ -38,3 +38,24 @@ export function parseProsePrefix(text: string): ProsePrefix {
|
|
|
38
38
|
}
|
|
39
39
|
return { kind: 'target', target, continueTurn, body };
|
|
40
40
|
}
|
|
41
|
+
|
|
42
|
+
/** A real hybrid routing line: exact triple-arrow plus an optional whitespace
|
|
43
|
+
* run and a non-whitespace target token. */
|
|
44
|
+
export const HYBRID_PROSE_PREFIX_LINE = /^>>>[ \t]*\S/;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Hybrid/locus-preserving routing grammar.
|
|
48
|
+
*
|
|
49
|
+
* `>>>#cafe body` and `>>> #cafe body` route this envelope explicitly while
|
|
50
|
+
* unprefixed prose remains ordinary frozen-locus speech. The authored source
|
|
51
|
+
* is never rewritten; callers consume this view only for publication.
|
|
52
|
+
*/
|
|
53
|
+
export function parseHybridProsePrefix(text: string): ProsePrefix {
|
|
54
|
+
const m = /^>>>[ \t]*(\S+)([ \t]+!)?[ \t]*\n?/.exec(text);
|
|
55
|
+
if (!m) return { kind: 'none', continueTurn: false, body: text };
|
|
56
|
+
const target = m[1]!;
|
|
57
|
+
const continueTurn = m[2] !== undefined;
|
|
58
|
+
const body = text.slice(m[0].length);
|
|
59
|
+
if (target === 'skip_reply') return { kind: 'private', continueTurn, body };
|
|
60
|
+
return { kind: 'target', target, continueTurn, body };
|
|
61
|
+
}
|
|
@@ -30,7 +30,7 @@ export interface RoutedDelta {
|
|
|
30
30
|
delta: string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
type State = 'line-start' | 'maybe-prefix' | 'prefix-token' | 'after-token' | 'body';
|
|
33
|
+
type State = 'line-start' | 'hybrid-indent' | 'maybe-prefix' | 'prefix-gap' | 'prefix-token' | 'after-token' | 'body';
|
|
34
34
|
|
|
35
35
|
export class ProseStreamRouter {
|
|
36
36
|
private target: string | null;
|
|
@@ -42,8 +42,8 @@ export class ProseStreamRouter {
|
|
|
42
42
|
|
|
43
43
|
constructor(
|
|
44
44
|
private opts: {
|
|
45
|
-
/**
|
|
46
|
-
mode: 'explicit' | 'locus';
|
|
45
|
+
/** explicit parses `>>`; hybrid parses `>>>` while retaining locus fallback. */
|
|
46
|
+
mode: 'explicit' | 'hybrid' | 'locus';
|
|
47
47
|
/** Initial destination: the frozen turn locus (locus mode) or null. */
|
|
48
48
|
initialTarget: string | null;
|
|
49
49
|
/** Resolve a `>>spec` to a channelId, or null (unresolved/ambiguous). */
|
|
@@ -59,6 +59,23 @@ export class ProseStreamRouter {
|
|
|
59
59
|
return this.accumulated;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Drop everything routed so far and return to the constructed state.
|
|
64
|
+
*
|
|
65
|
+
* For a membrane refusal retry (RetryingEvent): the abandoned attempt's
|
|
66
|
+
* prose must not survive into the new one, and `accumulated` feeds
|
|
67
|
+
* outgoing/complete — leaving it would finalize text the model never
|
|
68
|
+
* actually said in the attempt that stands.
|
|
69
|
+
*/
|
|
70
|
+
reset(): void {
|
|
71
|
+
this.target = this.opts.initialTarget;
|
|
72
|
+
this.state = this.opts.mode === 'locus' ? 'body' : 'line-start';
|
|
73
|
+
this.held = '';
|
|
74
|
+
this.token = '';
|
|
75
|
+
this.sawBang = false;
|
|
76
|
+
this.accumulated = new Map();
|
|
77
|
+
}
|
|
78
|
+
|
|
62
79
|
feed(delta: string): RoutedDelta[] {
|
|
63
80
|
const out: RoutedDelta[] = [];
|
|
64
81
|
for (const ch of delta) this.step(ch, out);
|
|
@@ -70,7 +87,12 @@ export class ProseStreamRouter {
|
|
|
70
87
|
* no body — dropped, matching delivery which would deliver empty body). */
|
|
71
88
|
finish(): RoutedDelta[] {
|
|
72
89
|
const out: RoutedDelta[] = [];
|
|
73
|
-
if (this.state === '
|
|
90
|
+
if (this.state === 'hybrid-indent') this.emit(this.held, out);
|
|
91
|
+
if (this.state === 'maybe-prefix') {
|
|
92
|
+
const full = this.opts.mode === 'hybrid' ? '>>>' : '>>';
|
|
93
|
+
const arrows = this.opts.mode === 'hybrid' ? this.held.trimStart() : this.held;
|
|
94
|
+
if (arrows !== full) this.emit(this.held, out);
|
|
95
|
+
}
|
|
74
96
|
// prefix-token / after-token at EOF: prefix without body — nothing to emit.
|
|
75
97
|
this.held = '';
|
|
76
98
|
this.state = 'line-start';
|
|
@@ -80,7 +102,7 @@ export class ProseStreamRouter {
|
|
|
80
102
|
private step(ch: string, out: RoutedDelta[]): void {
|
|
81
103
|
switch (this.state) {
|
|
82
104
|
case 'body':
|
|
83
|
-
if (ch === '\n' && this.opts.mode
|
|
105
|
+
if (ch === '\n' && this.opts.mode !== 'locus') {
|
|
84
106
|
this.emit('\n', out);
|
|
85
107
|
this.state = 'line-start';
|
|
86
108
|
} else {
|
|
@@ -89,34 +111,63 @@ export class ProseStreamRouter {
|
|
|
89
111
|
return;
|
|
90
112
|
|
|
91
113
|
case 'line-start':
|
|
114
|
+
if (this.opts.mode === 'hybrid' && (ch === ' ' || ch === '\t')) {
|
|
115
|
+
this.state = 'hybrid-indent';
|
|
116
|
+
this.held = ch;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
92
119
|
if (ch === '>') { this.state = 'maybe-prefix'; this.held = '>'; return; }
|
|
93
120
|
this.state = 'body';
|
|
94
121
|
this.step(ch, out);
|
|
95
122
|
return;
|
|
96
123
|
|
|
97
|
-
case '
|
|
98
|
-
if (
|
|
99
|
-
if (this.held
|
|
124
|
+
case 'hybrid-indent':
|
|
125
|
+
if (ch === ' ' || ch === '\t') { this.held += ch; return; }
|
|
126
|
+
if (ch === '>') { this.held += ch; this.state = 'maybe-prefix'; return; }
|
|
127
|
+
this.emit(this.held, out);
|
|
128
|
+
this.held = '';
|
|
129
|
+
this.state = 'body';
|
|
130
|
+
this.step(ch, out);
|
|
131
|
+
return;
|
|
132
|
+
|
|
133
|
+
case 'maybe-prefix': {
|
|
134
|
+
const prefix = this.opts.mode === 'hybrid' ? '>>>' : '>>';
|
|
135
|
+
const arrows = this.opts.mode === 'hybrid' ? this.held.trimStart() : this.held;
|
|
136
|
+
if (ch === '>' && arrows.length < prefix.length) {
|
|
137
|
+
this.held += '>';
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (arrows === prefix) {
|
|
141
|
+
if (this.opts.mode === 'hybrid' && (ch === ' ' || ch === '\t')) {
|
|
142
|
+
this.held = '';
|
|
143
|
+
this.state = 'prefix-gap';
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
100
146
|
if (ch === '\n' || ch === ' ' || ch === '\t') {
|
|
101
|
-
|
|
102
|
-
this.emit(this.held, out);
|
|
147
|
+
if (this.opts.mode === 'explicit') this.emit(this.held, out);
|
|
103
148
|
this.held = '';
|
|
104
|
-
this.state = 'body';
|
|
105
|
-
this.step(ch, out);
|
|
149
|
+
this.state = ch === '\n' ? 'line-start' : 'body';
|
|
150
|
+
if (ch !== '\n') this.step(ch, out);
|
|
106
151
|
return;
|
|
107
152
|
}
|
|
108
|
-
// real prefix begins
|
|
109
153
|
this.state = 'prefix-token';
|
|
110
154
|
this.token = ch;
|
|
111
155
|
this.held = '';
|
|
112
156
|
return;
|
|
113
157
|
}
|
|
114
|
-
// held === '>' and ch !== '>': plain body starting with '>'
|
|
115
158
|
this.emit(this.held, out);
|
|
116
159
|
this.held = '';
|
|
117
160
|
this.state = 'body';
|
|
118
161
|
this.step(ch, out);
|
|
119
162
|
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
case 'prefix-gap':
|
|
166
|
+
if (ch === ' ' || ch === '\t') return;
|
|
167
|
+
if (ch === '\n') { this.target = null; this.state = 'line-start'; return; }
|
|
168
|
+
this.state = 'prefix-token';
|
|
169
|
+
this.token = ch;
|
|
170
|
+
return;
|
|
120
171
|
|
|
121
172
|
case 'prefix-token':
|
|
122
173
|
if (ch === ' ' || ch === '\t') { this.applyTarget(); this.state = 'after-token'; this.sawBang = false; return; }
|
package/src/mcpl/push-handler.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
} from './types.js';
|
|
17
17
|
import type { FeatureSetManager } from './feature-set-manager.js';
|
|
18
18
|
import { McplFeatureSetError } from './feature-set-manager.js';
|
|
19
|
+
import { expandCoreTags } from './tags.js';
|
|
19
20
|
|
|
20
21
|
// ============================================================================
|
|
21
22
|
// McplPushEvent (the ProcessEvent shape pushed to the queue)
|
|
@@ -165,7 +166,18 @@ export class PushHandler {
|
|
|
165
166
|
params: PushEventParams,
|
|
166
167
|
responder?: Responder,
|
|
167
168
|
): void {
|
|
168
|
-
//
|
|
169
|
+
// §16.3: expand the normative chat:* core closure once, at entry, so
|
|
170
|
+
// every downstream consumer (wake matching, metadata, the queued event)
|
|
171
|
+
// sees the closed set. Producer `implies` edges are NOT consumed —
|
|
172
|
+
// advisory pending acceptance (§16.4). Tags were admitted before this
|
|
173
|
+
// point and grant nothing (§16.6).
|
|
174
|
+
if (params.tags) params.tags = expandCoreTags(params.tags);
|
|
175
|
+
// 1. Validate feature set. §6.6: rejection is diagnostics, not
|
|
176
|
+
// authorization, and MUST be a JSON-RPC error object — not a result
|
|
177
|
+
// carrying a failure flag. (The old `{accepted:false, reason}` result
|
|
178
|
+
// was AUDIT-001's finding: the error factories existed and were never
|
|
179
|
+
// invoked.) Falls back to the result shape only for a legacy responder
|
|
180
|
+
// with no error path.
|
|
169
181
|
try {
|
|
170
182
|
this.featureSetManager.validateInbound(serverId, params.featureSet);
|
|
171
183
|
} catch (err) {
|
|
@@ -175,7 +187,11 @@ export class PushHandler {
|
|
|
175
187
|
// Loud rejection — a rejected push event is an agent that silently
|
|
176
188
|
// never hears the message. (2026-07-09 diagnosability pass.)
|
|
177
189
|
console.error(`[push-event-rejected] server=${serverId} eventId=${params.eventId} reason=${reason}`);
|
|
178
|
-
|
|
190
|
+
if (err instanceof McplFeatureSetError && responder?.respondError) {
|
|
191
|
+
responder.respondError(err.code, reason, { featureSet: err.featureSet });
|
|
192
|
+
} else {
|
|
193
|
+
responder?.respond({ accepted: false, reason });
|
|
194
|
+
}
|
|
179
195
|
return;
|
|
180
196
|
}
|
|
181
197
|
|