@animalabs/agent-framework 0.26.2 → 0.27.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 +56 -6
- package/dist/src/agent.d.ts +263 -22
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +531 -54
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +1 -2
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/auto-continue.d.ts +33 -0
- package/dist/src/auto-continue.d.ts.map +1 -0
- package/dist/src/auto-continue.js +35 -0
- package/dist/src/auto-continue.js.map +1 -0
- package/dist/src/code-execution/isolate/scripts.d.ts +11 -0
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +5 -1
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/framework.d.ts +759 -129
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +3482 -928
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +309 -46
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +2117 -395
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/index.d.ts +2 -2
- package/dist/src/gate/index.d.ts.map +1 -1
- package/dist/src/gate/index.js +1 -1
- package/dist/src/gate/index.js.map +1 -1
- package/dist/src/gate/recipe-reconcile.d.ts +103 -0
- package/dist/src/gate/recipe-reconcile.d.ts.map +1 -0
- package/dist/src/gate/recipe-reconcile.js +325 -0
- package/dist/src/gate/recipe-reconcile.js.map +1 -0
- package/dist/src/gate/types.d.ts +68 -8
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/index.d.ts +6 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +433 -113
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +1152 -434
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +16 -8
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +29 -15
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts.map +1 -1
- package/dist/src/mcpl/convert-block.js +14 -2
- package/dist/src/mcpl/convert-block.js.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts +7 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.js +32 -7
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
- package/dist/src/mcpl/index.d.ts +5 -1
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js +4 -0
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/inference-router.d.ts +14 -2
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js +73 -28
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/prose-grammar.d.ts +4 -0
- package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
- package/dist/src/mcpl/prose-grammar.js +6 -0
- package/dist/src/mcpl/prose-grammar.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +3 -0
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +20 -2
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +33 -11
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/reference-fetcher.d.ts +63 -42
- package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
- package/dist/src/mcpl/reference-fetcher.js +177 -145
- package/dist/src/mcpl/reference-fetcher.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +380 -46
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +736 -77
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +82 -3
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +132 -7
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.d.ts +11 -0
- package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.js +21 -0
- package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
- package/dist/src/mcpl/tool-presence.d.ts +259 -0
- package/dist/src/mcpl/tool-presence.d.ts.map +1 -0
- package/dist/src/mcpl/tool-presence.js +628 -0
- package/dist/src/mcpl/tool-presence.js.map +1 -0
- package/dist/src/mcpl/tool-result-normalize.d.ts +9 -3
- package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
- package/dist/src/mcpl/tool-result-normalize.js +14 -6
- package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts +39 -0
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -0
- package/dist/src/mcpl/tool-signature.js +292 -0
- package/dist/src/mcpl/tool-signature.js.map +1 -0
- package/dist/src/mcpl/toolset-host.d.ts +200 -0
- package/dist/src/mcpl/toolset-host.d.ts.map +1 -0
- package/dist/src/mcpl/toolset-host.js +621 -0
- package/dist/src/mcpl/toolset-host.js.map +1 -0
- package/dist/src/mcpl/toolsets-module.d.ts +80 -0
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -0
- package/dist/src/mcpl/toolsets-module.js +431 -0
- package/dist/src/mcpl/toolsets-module.js.map +1 -0
- package/dist/src/mcpl/transport.d.ts +2 -2
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +3 -2
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +21 -1
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/mcpl/visible-content.d.ts +7 -9
- package/dist/src/mcpl/visible-content.d.ts.map +1 -1
- package/dist/src/mcpl/visible-content.js +21 -9
- package/dist/src/mcpl/visible-content.js.map +1 -1
- package/dist/src/module-registry.d.ts +23 -8
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +68 -15
- package/dist/src/module-registry.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 +2 -1
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +51 -17
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/history/semantic.d.ts +108 -6
- package/dist/src/modules/history/semantic.d.ts.map +1 -1
- package/dist/src/modules/history/semantic.js +273 -57
- package/dist/src/modules/history/semantic.js.map +1 -1
- package/dist/src/modules/workspace/direct-write.d.ts +181 -0
- package/dist/src/modules/workspace/direct-write.d.ts.map +1 -0
- package/dist/src/modules/workspace/direct-write.js +384 -0
- package/dist/src/modules/workspace/direct-write.js.map +1 -0
- package/dist/src/modules/workspace/edit-text.d.ts +26 -12
- package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
- package/dist/src/modules/workspace/edit-text.js +58 -28
- package/dist/src/modules/workspace/edit-text.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts +10 -0
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +1 -1
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +246 -27
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +1216 -236
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/read-page.d.ts +41 -0
- package/dist/src/modules/workspace/read-page.d.ts.map +1 -0
- package/dist/src/modules/workspace/read-page.js +213 -0
- package/dist/src/modules/workspace/read-page.js.map +1 -0
- package/dist/src/modules/workspace/result-text.d.ts +67 -0
- package/dist/src/modules/workspace/result-text.d.ts.map +1 -0
- package/dist/src/modules/workspace/result-text.js +94 -0
- package/dist/src/modules/workspace/result-text.js.map +1 -0
- package/dist/src/modules/workspace/text.d.ts +29 -12
- package/dist/src/modules/workspace/text.d.ts.map +1 -1
- package/dist/src/modules/workspace/text.js +35 -33
- package/dist/src/modules/workspace/text.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +41 -17
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/sent-rows.d.ts +59 -0
- package/dist/src/sent-rows.d.ts.map +1 -0
- package/dist/src/sent-rows.js +145 -0
- package/dist/src/sent-rows.js.map +1 -0
- package/dist/src/silent-heartbeat.d.ts +17 -10
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js.map +1 -1
- package/dist/src/stable-hash.d.ts +3 -0
- package/dist/src/stable-hash.d.ts.map +1 -0
- package/dist/src/stable-hash.js +15 -0
- package/dist/src/stable-hash.js.map +1 -0
- package/dist/src/stored-injections.d.ts +99 -0
- package/dist/src/stored-injections.d.ts.map +1 -0
- package/dist/src/stored-injections.js +106 -0
- package/dist/src/stored-injections.js.map +1 -0
- package/dist/src/text-as-stored.d.ts +4 -3
- package/dist/src/text-as-stored.d.ts.map +1 -1
- package/dist/src/text-as-stored.js +4 -3
- package/dist/src/text-as-stored.js.map +1 -1
- package/dist/src/thinking-drops.d.ts +81 -0
- package/dist/src/thinking-drops.d.ts.map +1 -0
- package/dist/src/thinking-drops.js +103 -0
- package/dist/src/thinking-drops.js.map +1 -0
- package/dist/src/tool-image-ledger.d.ts +9 -8
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js +9 -8
- package/dist/src/tool-image-ledger.js.map +1 -1
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +3 -1
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts +4 -2
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +21 -6
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tool-result-history.d.ts +176 -24
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +411 -91
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +17 -9
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +84 -37
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/types/agent.d.ts +186 -7
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +15 -1
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +24 -3
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +90 -7
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +57 -2
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +4 -3
- package/src/agent.ts +593 -60
- package/src/api/server.ts +1 -2
- package/src/auto-continue.ts +42 -0
- package/src/code-execution/isolate/scripts.ts +11 -3
- package/src/framework.ts +3601 -967
- package/src/gate/event-gate.ts +2315 -389
- package/src/gate/index.ts +2 -1
- package/src/gate/recipe-reconcile.ts +355 -0
- package/src/gate/types.ts +70 -8
- package/src/index.ts +7 -4
- package/src/mcpl/channel-registry.ts +1220 -467
- package/src/mcpl/conversation-router.ts +34 -16
- package/src/mcpl/convert-block.ts +14 -1
- package/src/mcpl/hook-orchestrator.ts +33 -6
- package/src/mcpl/index.ts +6 -1
- package/src/mcpl/inference-router.ts +79 -34
- package/src/mcpl/prose-grammar.ts +7 -0
- package/src/mcpl/protocol-family.ts +2 -0
- package/src/mcpl/push-coalescer.ts +51 -13
- package/src/mcpl/reference-fetcher.ts +201 -162
- package/src/mcpl/references.ts +851 -77
- package/src/mcpl/server-connection.ts +145 -7
- package/src/mcpl/tool-lifecycle.ts +21 -0
- package/src/mcpl/tool-presence.ts +650 -0
- package/src/mcpl/tool-result-normalize.ts +18 -6
- package/src/mcpl/tool-signature.ts +285 -0
- package/src/mcpl/toolset-host.ts +637 -0
- package/src/mcpl/toolsets-module.ts +451 -0
- package/src/mcpl/transport.ts +3 -2
- package/src/mcpl/types.ts +22 -1
- package/src/mcpl/visible-content.ts +22 -9
- package/src/module-registry.ts +77 -24
- package/src/modules/discord/index.ts +1 -1
- package/src/modules/history/index.ts +48 -18
- package/src/modules/history/semantic.ts +308 -54
- package/src/modules/workspace/direct-write.ts +425 -0
- package/src/modules/workspace/edit-text.ts +82 -35
- package/src/modules/workspace/effects.ts +1 -1
- package/src/modules/workspace/index.ts +1274 -241
- package/src/modules/workspace/read-page.ts +222 -0
- package/src/modules/workspace/result-text.ts +119 -0
- package/src/modules/workspace/text.ts +58 -32
- package/src/modules/workspace/types.ts +41 -17
- package/src/sent-rows.ts +162 -0
- package/src/silent-heartbeat.ts +17 -10
- package/src/stable-hash.ts +14 -0
- package/src/stored-injections.ts +131 -0
- package/src/text-as-stored.ts +4 -3
- package/src/thinking-drops.ts +141 -0
- package/src/tool-image-ledger.ts +9 -8
- package/src/tool-presentation.ts +3 -1
- package/src/tool-result-guard.ts +19 -6
- package/src/tool-result-history.ts +471 -90
- package/src/tune-out/coordinator.ts +80 -37
- package/src/types/agent.ts +189 -7
- package/src/types/events.ts +15 -1
- package/src/types/framework.ts +21 -3
- package/src/types/module.ts +86 -7
- package/src/types/trace.ts +58 -2
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
import { createHash } from 'node:crypto';
|
|
38
38
|
import type { ContextStrategy } from '@animalabs/context-manager';
|
|
39
39
|
import type { ChannelDescriptor } from './types.js';
|
|
40
|
+
import { destinationKey, type ChannelRef } from './channel-registry.js';
|
|
40
41
|
|
|
41
42
|
// ============================================================================
|
|
42
43
|
// Config & types
|
|
@@ -108,6 +109,9 @@ export interface ConversationRouterConfig {
|
|
|
108
109
|
|
|
109
110
|
export interface ConversationBinding {
|
|
110
111
|
channelId: string;
|
|
112
|
+
/** The channel's server: channel ids are unique only within one server's
|
|
113
|
+
* connection (MCPL §14.2), so each server's channel has its own binding. */
|
|
114
|
+
serverId: string;
|
|
111
115
|
agentName: string;
|
|
112
116
|
/** Engagement counter per channel — fresh forks get a new generation. */
|
|
113
117
|
generation: number;
|
|
@@ -157,11 +161,15 @@ export const DEFAULT_CLOSURE_PROMPT =
|
|
|
157
161
|
export class ConversationRouter {
|
|
158
162
|
private config: ConversationRouterConfig;
|
|
159
163
|
|
|
160
|
-
/** Active bindings, keyed by
|
|
164
|
+
/** Active bindings, keyed by the channel's server and id (destinationKey):
|
|
165
|
+
* two servers' channels with one id are two conversations. */
|
|
161
166
|
private bindings = new Map<string, ConversationBinding>();
|
|
162
167
|
|
|
163
|
-
/** Engagement counter per channel — survives unbind, so a re-bound
|
|
164
|
-
* gets a distinct fork agent name.
|
|
168
|
+
/** Engagement counter per channel id — survives unbind, so a re-bound
|
|
169
|
+
* channel gets a distinct fork agent name. Kept by bare id, as it's
|
|
170
|
+
* persisted: two servers sharing an id share a counter, which still never
|
|
171
|
+
* repeats a name, and their forks' names differ anyway, since each hashes
|
|
172
|
+
* its server (forkAgentName). */
|
|
165
173
|
private generations = new Map<string, number>();
|
|
166
174
|
|
|
167
175
|
constructor(config: ConversationRouterConfig) {
|
|
@@ -191,7 +199,7 @@ export class ConversationRouter {
|
|
|
191
199
|
* unchanged; a failed spawn doesn't leave a dangling route.
|
|
192
200
|
*/
|
|
193
201
|
route(facts: IncomingMessageFacts): RoutingDecision {
|
|
194
|
-
const existing = this.bindings.get(facts.channelId);
|
|
202
|
+
const existing = this.bindings.get(bindingKey({ serverId: facts.serverId ?? '', channelId: facts.channelId }));
|
|
195
203
|
|
|
196
204
|
if (existing) {
|
|
197
205
|
return {
|
|
@@ -223,39 +231,44 @@ export class ConversationRouter {
|
|
|
223
231
|
* Record a binding after the framework successfully spawned the fork.
|
|
224
232
|
* Returns the new binding.
|
|
225
233
|
*/
|
|
226
|
-
bind(
|
|
227
|
-
|
|
234
|
+
bind(channel: ChannelRef, agentName: string, generation: number, now = Date.now()): ConversationBinding {
|
|
235
|
+
// The counter is kept by bare id, and two servers' spawns on one id run
|
|
236
|
+
// beside each other, so a bind can finish after a later spawn's: the
|
|
237
|
+
// counter keeps the higher, or the next spawn would propose a fork name
|
|
238
|
+
// already used.
|
|
239
|
+
this.generations.set(channel.channelId, Math.max(this.generations.get(channel.channelId) ?? 0, generation));
|
|
228
240
|
const binding: ConversationBinding = {
|
|
229
|
-
channelId,
|
|
241
|
+
channelId: channel.channelId,
|
|
242
|
+
serverId: channel.serverId,
|
|
230
243
|
agentName,
|
|
231
244
|
generation,
|
|
232
245
|
boundAt: now,
|
|
233
246
|
lastActivity: now,
|
|
234
247
|
};
|
|
235
|
-
this.bindings.set(
|
|
248
|
+
this.bindings.set(bindingKey(channel), binding);
|
|
236
249
|
return binding;
|
|
237
250
|
}
|
|
238
251
|
|
|
239
252
|
/** Record delivered activity on an existing binding. Does not bind a
|
|
240
253
|
* channel or advance its generation. Call after successful delivery, not
|
|
241
254
|
* when previewing a route. The optional clock supports deterministic use. */
|
|
242
|
-
touch(
|
|
243
|
-
const binding = this.bindings.get(
|
|
255
|
+
touch(channel: ChannelRef, now = Date.now()): void {
|
|
256
|
+
const binding = this.bindings.get(bindingKey(channel));
|
|
244
257
|
if (binding) binding.lastActivity = now;
|
|
245
258
|
}
|
|
246
259
|
|
|
247
|
-
unbind(
|
|
248
|
-
this.bindings.delete(
|
|
260
|
+
unbind(channel: ChannelRef): void {
|
|
261
|
+
this.bindings.delete(bindingKey(channel));
|
|
249
262
|
}
|
|
250
263
|
|
|
251
|
-
getBinding(
|
|
252
|
-
return this.bindings.get(
|
|
264
|
+
getBinding(channel: ChannelRef): ConversationBinding | undefined {
|
|
265
|
+
return this.bindings.get(bindingKey(channel));
|
|
253
266
|
}
|
|
254
267
|
|
|
255
268
|
/** Reverse lookup: the channel an agent is bound to, if any. */
|
|
256
|
-
channelForAgent(agentName: string):
|
|
269
|
+
channelForAgent(agentName: string): ChannelRef | undefined {
|
|
257
270
|
for (const binding of this.bindings.values()) {
|
|
258
|
-
if (binding.agentName === agentName) return binding.channelId;
|
|
271
|
+
if (binding.agentName === agentName) return { serverId: binding.serverId, channelId: binding.channelId };
|
|
259
272
|
}
|
|
260
273
|
return undefined;
|
|
261
274
|
}
|
|
@@ -325,6 +338,11 @@ export class ConversationRouter {
|
|
|
325
338
|
/** The readable part of a fork's name stops here: uniqueness rides on the hash. */
|
|
326
339
|
const FORK_NAME_READABLE_MAX = 64;
|
|
327
340
|
|
|
341
|
+
/** A binding's key: its channel's server and id (destinationKey). */
|
|
342
|
+
function bindingKey(channel: ChannelRef): string {
|
|
343
|
+
return destinationKey(channel);
|
|
344
|
+
}
|
|
345
|
+
|
|
328
346
|
/**
|
|
329
347
|
* A fork's agent name: `<prefix>-<readable>-g<generation>-<hash>`, such as
|
|
330
348
|
* `conversation-C123-g2-e08e564271867d16` for Slack's channel `C123` on the
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
IMAGE_NO_DATA_TEXT,
|
|
12
12
|
IMAGE_NO_MEDIA_TYPE_TEXT,
|
|
13
13
|
INLINE_WITHHELD_TEXT,
|
|
14
|
+
classifyBlock,
|
|
14
15
|
isInlineContradiction,
|
|
15
16
|
referenceStubOrNull,
|
|
16
17
|
unknownBlockText,
|
|
@@ -85,12 +86,24 @@ export function convertBlock(block: McplContentBlock, provenance: string, server
|
|
|
85
86
|
}
|
|
86
87
|
return { type: 'text', text: isFilledString(block.data) ? AUDIO_NO_MEDIA_TYPE_TEXT : AUDIO_NO_DATA_TEXT };
|
|
87
88
|
|
|
88
|
-
case 'resource':
|
|
89
|
+
case 'resource': {
|
|
90
|
+
// MCP's embedded resource shows as the block its contents make would
|
|
91
|
+
// show here (RFC-005 §8.1), its uri never.
|
|
92
|
+
const c = classifyBlock(block);
|
|
93
|
+
if (c.kind === 'embedded' && c.contents.kind === 'block') {
|
|
94
|
+
return convertBlock(c.contents.block as McplContentBlock, provenance, serverId);
|
|
95
|
+
}
|
|
89
96
|
// RFC-005: reference blocks become bounded stubs — never raw URIs
|
|
90
97
|
// (a signed URL is a bearer credential that looks like a location).
|
|
91
98
|
return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ?? '[reference]' };
|
|
99
|
+
}
|
|
92
100
|
|
|
93
101
|
default: {
|
|
102
|
+
// MCP's link to a resource isn't one of MCPL's own blocks, but a server
|
|
103
|
+
// can send one, and RFC-005 §8.1 makes it a reference, shown as one.
|
|
104
|
+
if ((block as { type?: unknown }).type === 'resource_link') {
|
|
105
|
+
return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ?? '[reference]' };
|
|
106
|
+
}
|
|
94
107
|
// Unknown wire block types previously fell off the exhaustive switch
|
|
95
108
|
// and propagated `undefined` into ContentBlock[]. Fail visibly. A type
|
|
96
109
|
// that isn't a string is no type, and is never converted to one: an
|
|
@@ -71,7 +71,9 @@ function convertContent(content: string | McplContentBlock[], serverId: string):
|
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
73
|
* Convert an MCPL wire-format context injection, from the server `serverId`,
|
|
74
|
-
* to a context-manager ContextInjection.
|
|
74
|
+
* to a context-manager ContextInjection. It builds only these four fields,
|
|
75
|
+
* so a server can't opt an injection out of being stored
|
|
76
|
+
* (ModuleContextInjection.placedEachCompile, which only a module may set).
|
|
75
77
|
*/
|
|
76
78
|
function convertMcplInjection(mcplInj: McplContextInjection, serverId: string): ContextInjection {
|
|
77
79
|
return {
|
|
@@ -114,8 +116,17 @@ export class HookOrchestrator {
|
|
|
114
116
|
*
|
|
115
117
|
* Returns aggregated ContextInjection[] ready for context-manager's compile().
|
|
116
118
|
* Fail-open: servers that time out or error are silently skipped.
|
|
119
|
+
*
|
|
120
|
+
* `channelsFor` gives each server its own channel context (§14.4), in
|
|
121
|
+
* place of `params.channels`: channel ids are unique only within one
|
|
122
|
+
* server's connection (§14.2), so each server is told only of its own
|
|
123
|
+
* channels (ChannelRegistry.buildChannelContext). Undefined from it leaves
|
|
124
|
+
* the field out for that server.
|
|
117
125
|
*/
|
|
118
|
-
async beforeInference(
|
|
126
|
+
async beforeInference(
|
|
127
|
+
params: BeforeInferenceParams,
|
|
128
|
+
channelsFor?: (serverId: string) => BeforeInferenceParams['channels'],
|
|
129
|
+
): Promise<ContextInjection[]> {
|
|
119
130
|
// §5.4: the GRANT decides fan-out, not the raw advertisement — which
|
|
120
131
|
// also fixes selection for the recursive §5.1 shape, where
|
|
121
132
|
// `beforeInference: {observe: true, …}` is an object, not `true`. A
|
|
@@ -132,7 +143,7 @@ export class HookOrchestrator {
|
|
|
132
143
|
|
|
133
144
|
this._isInHook = true;
|
|
134
145
|
try {
|
|
135
|
-
return await this.fanOutBeforeInference(servers, params);
|
|
146
|
+
return await this.fanOutBeforeInference(servers, params, channelsFor);
|
|
136
147
|
} finally {
|
|
137
148
|
this._isInHook = false;
|
|
138
149
|
}
|
|
@@ -163,17 +174,33 @@ export class HookOrchestrator {
|
|
|
163
174
|
private async fanOutBeforeInference(
|
|
164
175
|
servers: McplServerConnection[],
|
|
165
176
|
params: BeforeInferenceParams,
|
|
177
|
+
channelsFor?: (serverId: string) => BeforeInferenceParams['channels'],
|
|
166
178
|
): Promise<ContextInjection[]> {
|
|
167
179
|
const results = await Promise.allSettled(
|
|
168
|
-
|
|
180
|
+
// Async, so a throw building one server's channel context fails that
|
|
181
|
+
// server alone, as its timeout or transport error would.
|
|
182
|
+
servers.map(async (server) => {
|
|
183
|
+
let base = params;
|
|
184
|
+
if (channelsFor) {
|
|
185
|
+
const { channels: _shared, ...rest } = params;
|
|
186
|
+
let channels: BeforeInferenceParams['channels'];
|
|
187
|
+
try {
|
|
188
|
+
channels = channelsFor(server.id);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
// A bug, not a server's failure: said, then this server is skipped.
|
|
191
|
+
console.error(`[beforeInference] the channel context for "${server.id}" couldn't be built, so it isn't called:`, err);
|
|
192
|
+
throw err;
|
|
193
|
+
}
|
|
194
|
+
base = channels ? { ...rest, channels } : rest;
|
|
195
|
+
}
|
|
169
196
|
// §10.1: a server not granted observe MUST receive
|
|
170
197
|
// `userMessage: null` — the field is ABSENT authority, not merely
|
|
171
198
|
// discouraged — while the hook is still invoked so granted
|
|
172
199
|
// injection positions keep working (write-without-read).
|
|
173
200
|
const perServer: BeforeInferenceParams =
|
|
174
201
|
CapabilityGrant.of(server).has('contextHooks.beforeInference.observe')
|
|
175
|
-
?
|
|
176
|
-
: { ...
|
|
202
|
+
? base
|
|
203
|
+
: { ...base, userMessage: null };
|
|
177
204
|
return withTimeout(
|
|
178
205
|
server.sendBeforeInference(perServer),
|
|
179
206
|
BEFORE_INFERENCE_TIMEOUT_MS,
|
package/src/mcpl/index.ts
CHANGED
|
@@ -88,6 +88,11 @@ export type {
|
|
|
88
88
|
} from './types.js';
|
|
89
89
|
|
|
90
90
|
export { McplMethod } from './types.js';
|
|
91
|
+
// RFC-014 tool presence and toolsets (host side).
|
|
92
|
+
export { ToolsetHub, ToolsetController, TOOLSETS_MODULE, META_TOOL_NAMES } from './toolset-host.js';
|
|
93
|
+
export { ToolsetsModule, TOOLSETS_REMINDER_NAMESPACE } from './toolsets-module.js';
|
|
94
|
+
export { ToolPresence } from './tool-presence.js';
|
|
95
|
+
export type { ChangeSet, MetaToolNames, ToolsetDecl } from './tool-presence.js';
|
|
91
96
|
|
|
92
97
|
// Error codes and factories
|
|
93
98
|
export {
|
|
@@ -171,7 +176,7 @@ export { InferenceRouter } from './inference-router.js';
|
|
|
171
176
|
|
|
172
177
|
// Channel registry (channel lifecycle, incoming messages, synthesized tools)
|
|
173
178
|
export { ChannelRegistry } from './channel-registry.js';
|
|
174
|
-
export type { PublishRefusalReason } from './channel-registry.js';
|
|
179
|
+
export type { ChannelRef, PublishRefusalReason } from './channel-registry.js';
|
|
175
180
|
|
|
176
181
|
// Inbound source envelope: where each accepted item came from, stamped once;
|
|
177
182
|
// and the digest of a delivered or stored body
|
|
@@ -26,6 +26,7 @@ import type {
|
|
|
26
26
|
import type { FeatureSetManager } from './feature-set-manager.js';
|
|
27
27
|
import { McplFeatureSetError } from './feature-set-manager.js';
|
|
28
28
|
import type { HookOrchestrator } from './hook-orchestrator.js';
|
|
29
|
+
import { logValue } from './log-value.js';
|
|
29
30
|
|
|
30
31
|
// ============================================================================
|
|
31
32
|
// Responder interface
|
|
@@ -181,6 +182,10 @@ export class InferenceRouter {
|
|
|
181
182
|
private featureSetManager: FeatureSetManager;
|
|
182
183
|
private routingPolicy: InferenceRoutingPolicy | null;
|
|
183
184
|
private emitTraceFn: (event: { type: string; [key: string]: unknown }) => void;
|
|
185
|
+
/** The refusals for want of a model that have been logged: one line per
|
|
186
|
+
* server with no policy, and per server and feature set with one, so a
|
|
187
|
+
* server that keeps asking doesn't flood the operator's log. */
|
|
188
|
+
private loggedRefusals = new Set<string>();
|
|
184
189
|
|
|
185
190
|
constructor(
|
|
186
191
|
membrane: Membrane,
|
|
@@ -201,7 +206,7 @@ export class InferenceRouter {
|
|
|
201
206
|
*
|
|
202
207
|
* 1. Validate feature set
|
|
203
208
|
* 2. Check loop prevention (reject if inside a hook)
|
|
204
|
-
* 3. Resolve model from routing policy
|
|
209
|
+
* 3. Resolve model from routing policy, refusing when it names none
|
|
205
210
|
* 4. Build membrane request
|
|
206
211
|
* 5. Run inference, streaming its visible text as chunks when asked
|
|
207
212
|
* 6. Answer with the result, or with the error for any ending that isn't
|
|
@@ -233,11 +238,62 @@ export class InferenceRouter {
|
|
|
233
238
|
return;
|
|
234
239
|
}
|
|
235
240
|
|
|
236
|
-
//
|
|
241
|
+
// Streaming needs a way to send chunks and an id to tag them with;
|
|
242
|
+
// without either, the request is answered whole.
|
|
243
|
+
const sendChunk = params.stream ? responder.sendChunk?.bind(responder) : undefined;
|
|
244
|
+
const chunkRequestId = sendChunk ? responder.requestId : undefined;
|
|
245
|
+
const streamed = chunkRequestId != null;
|
|
246
|
+
|
|
247
|
+
// 3. Resolve model. With no routing policy, or one that names no model
|
|
248
|
+
// for this feature set, the operator has chosen no model for this
|
|
249
|
+
// server's inference: refused rather than guessed, since a model picked
|
|
250
|
+
// here would run, on whatever credentials and at whatever price it
|
|
251
|
+
// carries, for a server the operator never chose it for. The answer is
|
|
252
|
+
// -32603, the code the router answers a failed inference with, as it
|
|
253
|
+
// answered this request when it reached the provider with no model; the
|
|
254
|
+
// server's grant can still list inferenceRequest, so not -32002.
|
|
237
255
|
const model = this.resolveModel(
|
|
238
256
|
params.featureSet,
|
|
239
257
|
params.conversationId,
|
|
240
258
|
);
|
|
259
|
+
if (model === undefined) {
|
|
260
|
+
const policy = Boolean(this.routingPolicy);
|
|
261
|
+
// One reason, quoted for where it goes: JSON for the server, logValue
|
|
262
|
+
// for the host log.
|
|
263
|
+
const reason = (quote: (value: string) => string) => policy
|
|
264
|
+
? `the inference routing policy (inferenceRouting) names no model for feature set ${quote(params.featureSet)}`
|
|
265
|
+
: 'the host sets no inference routing policy (inferenceRouting)';
|
|
266
|
+
const message = `inference/request refused: ${reason(JSON.stringify)}, so the host has no model to run it on; ` +
|
|
267
|
+
"it stays refused until the host's operator configures one";
|
|
268
|
+
// Logged once per server with no policy, and once per server and
|
|
269
|
+
// feature set with one, since each feature set the policy misses is
|
|
270
|
+
// its own entry to add; validateInbound (step 1) bounds feature sets
|
|
271
|
+
// to those the server declares.
|
|
272
|
+
const key = JSON.stringify(policy ? [serverId, params.featureSet] : [serverId]);
|
|
273
|
+
if (!this.loggedRefusals.has(key)) {
|
|
274
|
+
this.loggedRefusals.add(key);
|
|
275
|
+
console.error(
|
|
276
|
+
`[mcpl] ${logValue(serverId)}: inference/request refused: ${reason(logValue)}; ` +
|
|
277
|
+
(policy
|
|
278
|
+
? 'give it a default, or a byFeature or wildcards entry for that feature set. ' +
|
|
279
|
+
'This line prints once per server and feature set'
|
|
280
|
+
: 'set inferenceRouting in the framework config to choose the model server-initiated inference runs on. ' +
|
|
281
|
+
'This line prints once per server') +
|
|
282
|
+
'; later refusals are answered and traced but not logged.',
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
// The failure a trace watcher saw when this request failed at the
|
|
286
|
+
// provider, still seen; no inference_start, since none started.
|
|
287
|
+
this.emitTraceFn({
|
|
288
|
+
type: 'mcpl:inference_fail',
|
|
289
|
+
serverId,
|
|
290
|
+
model: null,
|
|
291
|
+
error: message,
|
|
292
|
+
streamed,
|
|
293
|
+
});
|
|
294
|
+
responder.respondError(-32603, message);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
241
297
|
|
|
242
298
|
// Emit trace: inference start
|
|
243
299
|
this.emitTraceFn({
|
|
@@ -263,12 +319,6 @@ export class InferenceRouter {
|
|
|
263
319
|
},
|
|
264
320
|
};
|
|
265
321
|
|
|
266
|
-
// Streaming needs a way to send chunks and an id to tag them with;
|
|
267
|
-
// without either, the request is answered whole.
|
|
268
|
-
const sendChunk = params.stream ? responder.sendChunk?.bind(responder) : undefined;
|
|
269
|
-
const chunkRequestId = sendChunk ? responder.requestId : undefined;
|
|
270
|
-
const streamed = chunkRequestId != null;
|
|
271
|
-
|
|
272
322
|
try {
|
|
273
323
|
// 5. Run inference. Both modes end in membrane's response, and one
|
|
274
324
|
// builder makes the result from it.
|
|
@@ -325,37 +375,32 @@ export class InferenceRouter {
|
|
|
325
375
|
* 2. Exact feature set match
|
|
326
376
|
* 3. Wildcard pattern match (first match wins)
|
|
327
377
|
* 4. Default from policy
|
|
328
|
-
*
|
|
378
|
+
*
|
|
379
|
+
* Only an entry naming a model counts: one that is empty or not a string
|
|
380
|
+
* names none and is passed over, as a missing one is. That includes what a
|
|
381
|
+
* server-sent conversation id or feature set such as `constructor` or
|
|
382
|
+
* `__proto__` finds on Object.prototype, which is never a string. A
|
|
383
|
+
* conversation id that is falsy reads no override.
|
|
384
|
+
*
|
|
385
|
+
* Undefined with no policy, or when none of these names a model: the
|
|
386
|
+
* request is then refused.
|
|
329
387
|
*/
|
|
330
388
|
private resolveModel(
|
|
331
389
|
featureSet: string,
|
|
332
390
|
conversationId?: string,
|
|
333
|
-
): string {
|
|
334
|
-
|
|
335
|
-
|
|
391
|
+
): string | undefined {
|
|
392
|
+
const policy = this.routingPolicy;
|
|
393
|
+
if (!policy) {
|
|
394
|
+
return undefined;
|
|
336
395
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
return this.routingPolicy.byFeature[featureSet];
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Check wildcard patterns
|
|
349
|
-
if (this.routingPolicy.wildcards) {
|
|
350
|
-
for (const [pattern, matchedModel] of Object.entries(this.routingPolicy.wildcards)) {
|
|
351
|
-
if (wildcardMatch(pattern, featureSet)) {
|
|
352
|
-
return matchedModel;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Default
|
|
358
|
-
return this.routingPolicy.default;
|
|
396
|
+
const named = (value: unknown): string | undefined =>
|
|
397
|
+
typeof value === 'string' && value !== '' ? value : undefined;
|
|
398
|
+
|
|
399
|
+
return (conversationId ? named(policy.overrides?.[conversationId]) : undefined)
|
|
400
|
+
?? named(policy.byFeature?.[featureSet])
|
|
401
|
+
// First wildcard that matches and names a model.
|
|
402
|
+
?? Object.entries(policy.wildcards ?? {}).find(([pattern, m]) => named(m) && wildcardMatch(pattern, featureSet))?.[1]
|
|
403
|
+
?? named(policy.default);
|
|
359
404
|
}
|
|
360
405
|
|
|
361
406
|
// ==========================================================================
|
|
@@ -19,6 +19,13 @@ export interface ProsePrefix {
|
|
|
19
19
|
body: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/** Whether a `>>` (or `>>>`) prefix line reads `name` as a target: one token,
|
|
23
|
+
* and not `skip_reply`, which the line reads as private speech. For a name
|
|
24
|
+
* offered to an agent to write after `>>`. */
|
|
25
|
+
export function isProseTargetToken(name: string): boolean {
|
|
26
|
+
return /^\S+$/.test(name) && name !== 'skip_reply';
|
|
27
|
+
}
|
|
28
|
+
|
|
22
29
|
/** A REAL prefix line: `>>` immediately followed by a target token. */
|
|
23
30
|
export const PROSE_PREFIX_LINE = /^>>\S/;
|
|
24
31
|
|
|
@@ -641,6 +641,8 @@ function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
|
641
641
|
export function mcplPolicyGrants(config: McplServerConfig): string[] {
|
|
642
642
|
const grants: string[] = [];
|
|
643
643
|
if (isEffective(config.allowHostCommands)) grants.push('allowHostCommands');
|
|
644
|
+
// RFC-014 tool hints are an MCPL advertisement.
|
|
645
|
+
if (config.toolHints === true) grants.push('toolHints');
|
|
644
646
|
// An allow-list, except that a pattern matching a path denied by default
|
|
645
647
|
// grants it (computeGrant's explicitlyEnabled), named by the path.
|
|
646
648
|
const enabled = config.enabledCapabilities as unknown;
|
|
@@ -127,6 +127,14 @@ interface DeferredBatch<E> {
|
|
|
127
127
|
latest: CoalescedOccurrence<E>;
|
|
128
128
|
notices: DeferredNotice[];
|
|
129
129
|
dropped: number;
|
|
130
|
+
/** Every notice folded into the batch, a dropped one included, and a
|
|
131
|
+
* plain occurrence a notice displaced unread (Occupant.ambient), was
|
|
132
|
+
* tagged `chat:ambient` (RFC-001), so its render asks nothing of the agent
|
|
133
|
+
* (CoalescerHost.wakeToRead). Snapshots carry it. A batch restored
|
|
134
|
+
* without it, from a snapshot written before it existed or from an
|
|
135
|
+
* acceptance receipt, which doesn't carry it, reads as not all ambient:
|
|
136
|
+
* the side that keeps a wake. */
|
|
137
|
+
allAmbient?: true;
|
|
130
138
|
/** Set on a batch restored from a snapshot whose render may have started. */
|
|
131
139
|
noRender?: boolean;
|
|
132
140
|
/** The batch opened on the agent's first view of its subject: a fresh
|
|
@@ -184,10 +192,22 @@ interface Occupant {
|
|
|
184
192
|
* render that joins a run finds a version placed. In memory only: a
|
|
185
193
|
* snapshot's occupant comes back as read. */
|
|
186
194
|
firstView?: true;
|
|
195
|
+
/** A plain occurrence its server tagged `chat:ambient`. A notice that
|
|
196
|
+
* displaces it unread stands its batch's render in its place, so that
|
|
197
|
+
* render is all ambient only if this was (DeferredBatch.allAmbient). In
|
|
198
|
+
* memory only, as `firstView`: a snapshot's occupant comes back as read,
|
|
199
|
+
* and only an unread one is displaced. */
|
|
200
|
+
ambient?: true;
|
|
187
201
|
/** The run's earlier renders, oldest first, each placed ahead of this one. */
|
|
188
202
|
before?: Array<Omit<Occupant, 'before'>>;
|
|
189
203
|
}
|
|
190
204
|
|
|
205
|
+
/** An occupant's `ambient` mark, for a plain occurrence its server tagged
|
|
206
|
+
* `chat:ambient`. */
|
|
207
|
+
function ambientOccupant(occurrence: { tags?: string[] }): { ambient?: true } {
|
|
208
|
+
return occurrence.tags?.includes('chat:ambient') === true ? { ambient: true } : {};
|
|
209
|
+
}
|
|
210
|
+
|
|
191
211
|
/** The occupant holding a run's renders, given oldest first: the newest,
|
|
192
212
|
* carrying the rest as `before`. */
|
|
193
213
|
function runOf(members: Array<Omit<Occupant, 'before'>>): Occupant | undefined {
|
|
@@ -246,7 +266,7 @@ export interface CoalescingSnapshot {
|
|
|
246
266
|
firstViewEnded?: true;
|
|
247
267
|
/** `firstView` only on a batch that wasn't rendering: one that was may
|
|
248
268
|
* have been shown. */
|
|
249
|
-
batch?: { latest: CoalescedOccurrence<unknown>; notices: DeferredNotice[]; dropped: number; rendering?: boolean; firstView?: true };
|
|
269
|
+
batch?: { latest: CoalescedOccurrence<unknown>; notices: DeferredNotice[]; dropped: number; rendering?: boolean; firstView?: true; allAmbient?: true };
|
|
250
270
|
}>;
|
|
251
271
|
}
|
|
252
272
|
/** One receipt, written synchronously at acceptance (bridges the snapshot throttle). */
|
|
@@ -299,8 +319,11 @@ export interface CoalescerHost<E> {
|
|
|
299
319
|
/** Queue an ordinary push wake for `agent`, under the batch's own wake
|
|
300
320
|
* policy, to read a render of the batch placed in its deferred queue that
|
|
301
321
|
* no request consumed, at `deferredId`. It carries no subject, so the
|
|
302
|
-
* subject's other wakes, and later withdrawals of them, leave it.
|
|
303
|
-
|
|
322
|
+
* subject's other wakes, and later withdrawals of them, leave it.
|
|
323
|
+
* `allAmbient`: every notice the render stands for was tagged
|
|
324
|
+
* `chat:ambient` (DeferredBatch.allAmbient), so the wake is marked
|
|
325
|
+
* ambient. */
|
|
326
|
+
wakeToRead(occurrence: CoalescedOccurrence<E>, agent: string, deferredId?: string, allAmbient?: boolean): Promise<void>;
|
|
304
327
|
/** Withdraw unstarted wakes whose sole cause is this subject. */
|
|
305
328
|
cancelWake(subject: string): void;
|
|
306
329
|
/** The batch's audience still holds the authority it was admitted under. */
|
|
@@ -416,14 +439,20 @@ export function validateCoalescedContent(
|
|
|
416
439
|
/**
|
|
417
440
|
* Whether the converter (`convertBlock` in convert-block.ts, which every lane
|
|
418
441
|
* uses) shows a block as itself, its text or its image or
|
|
419
|
-
* audio source
|
|
442
|
+
* audio source (for MCP's embedded resource, its contents'), or as a
|
|
443
|
+
* reference stub, and not as a placeholder: an
|
|
420
444
|
* unrecognized block, a no-data or no-media-type one, withheld inline data,
|
|
421
445
|
* or a text block whose text isn't a string. This follows its branches in
|
|
422
446
|
* their order.
|
|
423
447
|
*/
|
|
424
448
|
function showsAsItself(b: Record<string, unknown>): boolean {
|
|
425
449
|
if (b.type === 'text') return typeof b.text === 'string';
|
|
426
|
-
if (b.type === 'resource'
|
|
450
|
+
if (b.type === 'resource' || b.type === 'resource_link') {
|
|
451
|
+
const c = classifyBlock(b);
|
|
452
|
+
// MCP's embedded resource shows as the block its contents make.
|
|
453
|
+
if (c.kind === 'embedded') return c.contents.kind === 'block' && showsAsItself(c.contents.block);
|
|
454
|
+
return c.kind === 'reference';
|
|
455
|
+
}
|
|
427
456
|
if (b.type !== 'image' && b.type !== 'audio') return false;
|
|
428
457
|
if (isInlineContradiction(b)) return false;
|
|
429
458
|
if (isFilledString(b.uri) && b.disposition !== undefined) return classifyBlock(b).kind === 'reference';
|
|
@@ -833,7 +862,7 @@ export class PushCoalescer<E = unknown> {
|
|
|
833
862
|
// context that read a version, never a new one (§3.2).
|
|
834
863
|
const placement = await this.deliver({ ...occurrence, deliverTo: state.audienceAgent });
|
|
835
864
|
if (placement) {
|
|
836
|
-
state.occupant = { eventId: occurrence.eventId, placement };
|
|
865
|
+
state.occupant = { eventId: occurrence.eventId, placement, ...ambientOccupant(occurrence) };
|
|
837
866
|
state.audienceAgent = placement.agent;
|
|
838
867
|
}
|
|
839
868
|
return 'noted';
|
|
@@ -858,7 +887,7 @@ export class PushCoalescer<E = unknown> {
|
|
|
858
887
|
...occurrence, deliverTo: prior.placement.agent,
|
|
859
888
|
...(firstView ? { firstView: true } : {}),
|
|
860
889
|
});
|
|
861
|
-
state.occupant = placement ? { eventId: occurrence.eventId, placement, ...(firstView ? { firstView: true as const } : {}) } : undefined;
|
|
890
|
+
state.occupant = placement ? { eventId: occurrence.eventId, placement, ...(firstView ? { firstView: true as const } : {}), ...ambientOccupant(occurrence) } : undefined;
|
|
862
891
|
if (placement) state.audienceAgent = placement.agent;
|
|
863
892
|
return 'replaced';
|
|
864
893
|
}
|
|
@@ -872,7 +901,7 @@ export class PushCoalescer<E = unknown> {
|
|
|
872
901
|
// batch's place (a batch's own first view ends with the subject's).
|
|
873
902
|
const firstView = hadBatch ? batchFirstView && state.history === 'none' : outcome === 'first' && !state.firstViewEnded;
|
|
874
903
|
const placement = await this.deliver(hadBatch && firstView ? { ...occurrence, firstView: true } : occurrence);
|
|
875
|
-
state.occupant = placement ? { eventId: occurrence.eventId, placement, ...(firstView ? { firstView: true as const } : {}) } : undefined;
|
|
904
|
+
state.occupant = placement ? { eventId: occurrence.eventId, placement, ...(firstView ? { firstView: true as const } : {}), ...ambientOccupant(occurrence) } : undefined;
|
|
876
905
|
if (placement) state.audienceAgent = placement.agent;
|
|
877
906
|
return outcome;
|
|
878
907
|
}
|
|
@@ -887,6 +916,10 @@ export class PushCoalescer<E = unknown> {
|
|
|
887
916
|
// batches already open too). A render still under way is weighed when a
|
|
888
917
|
// batch's first delivery is.
|
|
889
918
|
let firstView = state.history === 'none' && !state.firstViewEnded;
|
|
919
|
+
// Whether every notice the batch's render stands for was tagged
|
|
920
|
+
// `chat:ambient` (DeferredBatch.allAmbient): this one, and a plain
|
|
921
|
+
// occurrence it displaces unread, whose place the render takes.
|
|
922
|
+
let ambient = occurrence.tags?.includes('chat:ambient') === true;
|
|
890
923
|
if (occupantUnread && state.occupant?.rendered) {
|
|
891
924
|
// An unread render is a delta the next render builds on (§5.2), not a
|
|
892
925
|
// plain occurrence: it stays, and the batch follows its agent (§3.2), so
|
|
@@ -897,6 +930,7 @@ export class PushCoalescer<E = unknown> {
|
|
|
897
930
|
// §5.1: a notice displaces an unread plain occurrence and opens a batch
|
|
898
931
|
// — in the audience that held the plain occurrence (§3.2).
|
|
899
932
|
if (!this.occupantLeaves(state, state.occupant)) firstView = false;
|
|
933
|
+
if (state.occupant.ambient !== true) ambient = false;
|
|
900
934
|
this.host.audit({ kind: 'displaced', subject, eventId: state.occupant.eventId });
|
|
901
935
|
if (this.host.remove(state.occupant.placement)) {
|
|
902
936
|
outcome = 'replaced';
|
|
@@ -913,10 +947,11 @@ export class PushCoalescer<E = unknown> {
|
|
|
913
947
|
state.batch.notices.push(notice);
|
|
914
948
|
if (state.batch.notices.length > this.maxNotices) { state.batch.notices.shift(); state.batch.dropped++; }
|
|
915
949
|
state.batch.latest = occurrence;
|
|
950
|
+
if (!ambient) delete state.batch.allAmbient;
|
|
916
951
|
outcome = 'replaced';
|
|
917
952
|
} else {
|
|
918
953
|
// Rule 1: a notice during a render opens a NEW batch ("first").
|
|
919
|
-
state.batch = { latest: occurrence, notices: [notice], dropped: 0, ...(firstView ? { firstView: true as const } : {}) };
|
|
954
|
+
state.batch = { latest: occurrence, notices: [notice], dropped: 0, ...(firstView ? { firstView: true as const } : {}), ...(ambient ? { allAmbient: true as const } : {}) };
|
|
920
955
|
}
|
|
921
956
|
await this.host.wakeForBatch(occurrence);
|
|
922
957
|
return outcome;
|
|
@@ -1122,7 +1157,9 @@ export class PushCoalescer<E = unknown> {
|
|
|
1122
1157
|
// agent's wake for this batch; give it an ordinary wake, as any
|
|
1123
1158
|
// delivery during its turn has, beside any wakes a newer batch has,
|
|
1124
1159
|
// so that its next turn reads the content (the wake starts no turn
|
|
1125
|
-
// once
|
|
1160
|
+
// once a request that carried it is answered and, if the wake
|
|
1161
|
+
// carries a channel and the batch wasn't all ambient, the agent has
|
|
1162
|
+
// published there since, or once it's removed).
|
|
1126
1163
|
// The run's renders keep their own records, whatever else an
|
|
1127
1164
|
// occupant carries, a first view's mark included.
|
|
1128
1165
|
let before: Occupant['before'];
|
|
@@ -1136,7 +1173,7 @@ export class PushCoalescer<E = unknown> {
|
|
|
1136
1173
|
await this.host.wakeForBatch(state.batch.latest);
|
|
1137
1174
|
}
|
|
1138
1175
|
state.audienceAgent = placement.agent;
|
|
1139
|
-
await this.host.wakeToRead(occurrence, placement.agent, placement.deferredId);
|
|
1176
|
+
await this.host.wakeToRead(occurrence, placement.agent, placement.deferredId, batch.allAmbient === true);
|
|
1140
1177
|
return;
|
|
1141
1178
|
}
|
|
1142
1179
|
// The materialized occurrence is consumed by the request being
|
|
@@ -1180,6 +1217,7 @@ export class PushCoalescer<E = unknown> {
|
|
|
1180
1217
|
state.batch = {
|
|
1181
1218
|
latest: s.batch.latest as CoalescedOccurrence<E>, notices: s.batch.notices, dropped: s.batch.dropped,
|
|
1182
1219
|
...(s.batch.rendering ? { noRender: true } : s.batch.firstView ? { firstView: true as const } : {}),
|
|
1220
|
+
...(s.batch.allAmbient ? { allAmbient: true as const } : {}),
|
|
1183
1221
|
};
|
|
1184
1222
|
}
|
|
1185
1223
|
this.subjects.set(s.subject, state);
|
|
@@ -1265,8 +1303,8 @@ export class PushCoalescer<E = unknown> {
|
|
|
1265
1303
|
// restore() reads only these. After a restart nothing of the subject
|
|
1266
1304
|
// is tracked as unread, a run included: its history counts as read.
|
|
1267
1305
|
...(s.occupant ? { occupant: { eventId: s.occupant.eventId, placement: s.occupant.placement } } : {}),
|
|
1268
|
-
...(s.batch ? { batch: { latest: s.batch.latest as CoalescedOccurrence<unknown>, notices: s.batch.notices, dropped: s.batch.dropped, ...(s.batch.noRender ? { rendering: true } : s.batch.firstView ? { firstView: true as const } : {}) } }
|
|
1269
|
-
: s.rendering ? { batch: { latest: s.rendering.batch.latest as CoalescedOccurrence<unknown>, notices: s.rendering.batch.notices, dropped: s.rendering.batch.dropped, rendering: true } }
|
|
1306
|
+
...(s.batch ? { batch: { latest: s.batch.latest as CoalescedOccurrence<unknown>, notices: s.batch.notices, dropped: s.batch.dropped, ...(s.batch.noRender ? { rendering: true } : s.batch.firstView ? { firstView: true as const } : {}), ...(s.batch.allAmbient ? { allAmbient: true as const } : {}) } }
|
|
1307
|
+
: s.rendering ? { batch: { latest: s.rendering.batch.latest as CoalescedOccurrence<unknown>, notices: s.rendering.batch.notices, dropped: s.rendering.batch.dropped, rendering: true, ...(s.rendering.batch.allAmbient ? { allAmbient: true as const } : {}) } }
|
|
1270
1308
|
: {}),
|
|
1271
1309
|
})),
|
|
1272
1310
|
};
|