@animalabs/agent-framework 0.22.4 → 0.23.1
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 +105 -3
- package/dist/src/agent.d.ts +36 -10
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +76 -32
- package/dist/src/agent.js.map +1 -1
- package/dist/src/code-execution/index.d.ts +1 -1
- package/dist/src/code-execution/index.d.ts.map +1 -1
- package/dist/src/code-execution/index.js.map +1 -1
- package/dist/src/code-execution/py-runner.d.ts +34 -2
- package/dist/src/code-execution/py-runner.d.ts.map +1 -1
- package/dist/src/code-execution/py-runner.js +72 -16
- package/dist/src/code-execution/py-runner.js.map +1 -1
- package/dist/src/code-execution/runtime-py.d.ts +3 -2
- package/dist/src/code-execution/runtime-py.d.ts.map +1 -1
- package/dist/src/code-execution/runtime-py.js +47 -7
- package/dist/src/code-execution/runtime-py.js.map +1 -1
- package/dist/src/code-execution/tool-definition.d.ts +12 -7
- package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
- package/dist/src/code-execution/tool-definition.js +59 -11
- package/dist/src/code-execution/tool-definition.js.map +1 -1
- package/dist/src/config-checks.d.ts +2 -2
- package/dist/src/config-checks.d.ts.map +1 -1
- package/dist/src/config-checks.js +6 -2
- package/dist/src/config-checks.js.map +1 -1
- package/dist/src/framework.d.ts +744 -120
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +3546 -906
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +13 -6
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/held-writes.d.ts +16 -0
- package/dist/src/held-writes.d.ts.map +1 -0
- package/dist/src/held-writes.js +56 -0
- package/dist/src/held-writes.js.map +1 -0
- package/dist/src/index.d.ts +7 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +8 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/capability-grant.d.ts +4 -0
- package/dist/src/mcpl/capability-grant.d.ts.map +1 -1
- package/dist/src/mcpl/capability-grant.js +1 -1
- package/dist/src/mcpl/capability-grant.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +95 -18
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +578 -150
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts +2 -2
- package/dist/src/mcpl/convert-block.d.ts.map +1 -1
- package/dist/src/mcpl/convert-block.js +6 -7
- package/dist/src/mcpl/convert-block.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 +3 -1
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/inference-router.d.ts +4 -2
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js +6 -7
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/log-value.d.ts +7 -0
- package/dist/src/mcpl/log-value.d.ts.map +1 -1
- package/dist/src/mcpl/log-value.js +9 -0
- package/dist/src/mcpl/log-value.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts +227 -0
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -0
- package/dist/src/mcpl/modern-connection.js +1088 -0
- package/dist/src/mcpl/modern-connection.js.map +1 -0
- package/dist/src/mcpl/prefix-rule.d.ts +18 -0
- package/dist/src/mcpl/prefix-rule.d.ts.map +1 -0
- package/dist/src/mcpl/prefix-rule.js +25 -0
- package/dist/src/mcpl/prefix-rule.js.map +1 -0
- package/dist/src/mcpl/protocol-family.d.ts +179 -0
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -0
- package/dist/src/mcpl/protocol-family.js +697 -0
- package/dist/src/mcpl/protocol-family.js.map +1 -0
- package/dist/src/mcpl/push-coalescer.d.ts +38 -19
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +52 -23
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/reference-fetcher.d.ts +66 -15
- package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
- package/dist/src/mcpl/reference-fetcher.js +243 -76
- package/dist/src/mcpl/reference-fetcher.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +63 -18
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +117 -32
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +326 -30
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +995 -212
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/server-registry.d.ts +25 -5
- package/dist/src/mcpl/server-registry.d.ts.map +1 -1
- package/dist/src/mcpl/server-registry.js +51 -12
- package/dist/src/mcpl/server-registry.js.map +1 -1
- package/dist/src/mcpl/tool-result-normalize.d.ts +54 -0
- package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -0
- package/dist/src/mcpl/tool-result-normalize.js +232 -0
- package/dist/src/mcpl/tool-result-normalize.js.map +1 -0
- package/dist/src/mcpl/transport.d.ts +56 -13
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +206 -70
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +66 -20
- 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 +2 -1
- package/dist/src/mcpl/visible-content.d.ts.map +1 -1
- package/dist/src/mcpl/visible-content.js +2 -1
- package/dist/src/mcpl/visible-content.js.map +1 -1
- package/dist/src/module-registry.d.ts +16 -6
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +65 -6
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/api/index.d.ts +1 -1
- package/dist/src/modules/api/index.d.ts.map +1 -1
- package/dist/src/modules/api/index.js +3 -2
- package/dist/src/modules/api/index.js.map +1 -1
- package/dist/src/modules/discord/discord-js-client.d.ts +42 -2
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
- package/dist/src/modules/discord/discord-js-client.js +268 -65
- package/dist/src/modules/discord/discord-js-client.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts +27 -5
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +366 -76
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/discord/refusal.d.ts +14 -0
- package/dist/src/modules/discord/refusal.d.ts.map +1 -0
- package/dist/src/modules/discord/refusal.js +14 -0
- package/dist/src/modules/discord/refusal.js.map +1 -0
- package/dist/src/modules/discord/types.d.ts +17 -7
- package/dist/src/modules/discord/types.d.ts.map +1 -1
- package/dist/src/modules/health/index.d.ts.map +1 -1
- package/dist/src/modules/health/index.js +6 -2
- package/dist/src/modules/health/index.js.map +1 -1
- package/dist/src/modules/index.d.ts +1 -1
- package/dist/src/modules/index.d.ts.map +1 -1
- package/dist/src/modules/index.js +1 -1
- package/dist/src/modules/index.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +25 -2
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +45 -5
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts +2 -1
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +19 -9
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +214 -42
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +667 -113
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +240 -8
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +554 -24
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +10 -1
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts +42 -0
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +130 -25
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/silent-heartbeat.d.ts +112 -39
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js +100 -29
- package/dist/src/silent-heartbeat.js.map +1 -1
- package/dist/src/tool-args.d.ts +40 -0
- package/dist/src/tool-args.d.ts.map +1 -0
- package/dist/src/tool-args.js +207 -0
- package/dist/src/tool-args.js.map +1 -0
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +9 -2
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/types/agent.d.ts +27 -17
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +8 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +17 -2
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +54 -10
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +2 -2
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -4
- package/src/agent.ts +99 -36
- package/src/code-execution/index.ts +1 -0
- package/src/code-execution/py-runner.ts +104 -17
- package/src/code-execution/runtime-py.ts +47 -7
- package/src/code-execution/tool-definition.ts +65 -11
- package/src/config-checks.ts +4 -2
- package/src/framework.ts +3633 -956
- package/src/gate/event-gate.ts +14 -6
- package/src/held-writes.ts +59 -0
- package/src/index.ts +19 -4
- package/src/mcpl/capability-grant.ts +1 -1
- package/src/mcpl/channel-registry.ts +613 -160
- package/src/mcpl/convert-block.ts +15 -7
- package/src/mcpl/index.ts +14 -1
- package/src/mcpl/inference-router.ts +8 -7
- package/src/mcpl/log-value.ts +10 -0
- package/src/mcpl/modern-connection.ts +1182 -0
- package/src/mcpl/prefix-rule.ts +32 -0
- package/src/mcpl/protocol-family.ts +691 -0
- package/src/mcpl/push-coalescer.ts +67 -29
- package/src/mcpl/reference-fetcher.ts +288 -82
- package/src/mcpl/references.ts +123 -33
- package/src/mcpl/server-connection.ts +1050 -221
- package/src/mcpl/server-registry.ts +50 -12
- package/src/mcpl/tool-result-normalize.ts +294 -0
- package/src/mcpl/transport.ts +204 -62
- package/src/mcpl/types.ts +67 -20
- package/src/mcpl/visible-content.ts +2 -1
- package/src/module-registry.ts +68 -13
- package/src/modules/api/index.ts +3 -2
- package/src/modules/discord/discord-js-client.ts +288 -72
- package/src/modules/discord/index.ts +380 -77
- package/src/modules/discord/refusal.ts +13 -0
- package/src/modules/discord/types.ts +17 -9
- package/src/modules/health/index.ts +6 -2
- package/src/modules/index.ts +1 -1
- package/src/modules/workspace/disk-agreement.ts +46 -5
- package/src/modules/workspace/effects.ts +18 -7
- package/src/modules/workspace/index.ts +722 -124
- package/src/modules/workspace/observe.ts +591 -22
- package/src/modules/workspace/types.ts +10 -1
- package/src/modules/workspace/watcher.ts +126 -25
- package/src/silent-heartbeat.ts +184 -59
- package/src/tool-args.ts +213 -0
- package/src/tool-presentation.ts +9 -2
- package/src/types/agent.ts +27 -17
- package/src/types/events.ts +8 -0
- package/src/types/framework.ts +17 -2
- package/src/types/module.ts +55 -10
- package/src/types/trace.ts +2 -2
package/src/mcpl/transport.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { StringDecoder } from 'node:string_decoder';
|
|
|
23
23
|
import WebSocket from 'ws';
|
|
24
24
|
|
|
25
25
|
import type { McplServerConfig } from './types.js';
|
|
26
|
+
import { describeCredentialError, noteDialSent, redactUrl, resolveServerBinding, urlAsDialed } from './protocol-family.js';
|
|
26
27
|
import { assertBooleanSetting } from '../config-checks.js';
|
|
27
28
|
import { logDetail } from './log-value.js';
|
|
28
29
|
|
|
@@ -45,6 +46,9 @@ export interface TransportCloseInfo {
|
|
|
45
46
|
* - `'stderr'` `(line: string)` diagnostic line (stdio child stderr; never WS)
|
|
46
47
|
* - `'error'` `(err: Error)` transport-level error
|
|
47
48
|
* - `'close'` `(info: TransportCloseInfo)` transport closed (once)
|
|
49
|
+
* - `'unreaped'` `(failure: Error)` a stdio child whose pipe failed
|
|
50
|
+
* outlived SIGKILL too: the transport stays open, owning the child, and
|
|
51
|
+
* close() retries the reap (never WS)
|
|
48
52
|
*
|
|
49
53
|
* Inbound `'line'`/`'stderr'` events are buffered until the first matching
|
|
50
54
|
* listener attaches (and re-buffered whenever there are none), so no message is
|
|
@@ -90,6 +94,17 @@ export abstract class McplTransport extends EventEmitter {
|
|
|
90
94
|
return '';
|
|
91
95
|
}
|
|
92
96
|
|
|
97
|
+
/**
|
|
98
|
+
* The error a stdio child's stdin or stdout failed with, once one has. The
|
|
99
|
+
* transport is ending the child then, and stays open until it exits, but
|
|
100
|
+
* writes nothing more to it: writeLine drops each line, and both engines
|
|
101
|
+
* refuse a request as not sent. Undefined while writes go out, and always
|
|
102
|
+
* for a transport that has no pipes.
|
|
103
|
+
*/
|
|
104
|
+
get pipeFailure(): Error | undefined {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
93
108
|
/**
|
|
94
109
|
* Resolves once the server's diagnostic stream has ended or closed, so that
|
|
95
110
|
* {@link stderrTail} holds what arrived on it, or once `ms` have passed while
|
|
@@ -167,7 +182,8 @@ export const CHILD_ENV_ALLOWLIST = [
|
|
|
167
182
|
'http_proxy', 'https_proxy', 'no_proxy',
|
|
168
183
|
'__CF_USER_TEXT_ENCODING',
|
|
169
184
|
// Windows equivalents, harmless elsewhere.
|
|
170
|
-
|
|
185
|
+
// USERNAME is Windows' USER, which Python's getpass.getuser() reads there.
|
|
186
|
+
'SYSTEMROOT', 'WINDIR', 'COMSPEC', 'PATHEXT', 'APPDATA', 'LOCALAPPDATA', 'USERPROFILE', 'USERNAME',
|
|
171
187
|
];
|
|
172
188
|
|
|
173
189
|
/** Environment for a stdio child: allowlisted host vars (+ LC_*), then the
|
|
@@ -244,6 +260,10 @@ export class StdioTransport extends McplTransport {
|
|
|
244
260
|
return (this.stderrCut ? '…' : '') + this.stderrEnd;
|
|
245
261
|
}
|
|
246
262
|
|
|
263
|
+
override get pipeFailure(): Error | undefined {
|
|
264
|
+
return this.pipeError;
|
|
265
|
+
}
|
|
266
|
+
|
|
247
267
|
override stderrEnded(ms: number): Promise<void> {
|
|
248
268
|
const stderr = this.child.stderr;
|
|
249
269
|
if (!stderr || this.stderrDone) return Promise.resolve();
|
|
@@ -266,6 +286,8 @@ export class StdioTransport extends McplTransport {
|
|
|
266
286
|
private constructor(
|
|
267
287
|
private readonly child: ChildProcess,
|
|
268
288
|
private readonly rl: ReadlineInterface,
|
|
289
|
+
/** The server this child serves, named in its stderr lines and in what
|
|
290
|
+
* its reaping reports. */
|
|
269
291
|
serverId: string,
|
|
270
292
|
) {
|
|
271
293
|
super(serverId);
|
|
@@ -301,16 +323,26 @@ export class StdioTransport extends McplTransport {
|
|
|
301
323
|
this.child.on('error', (err) => this.reportError(err));
|
|
302
324
|
// The child's stdin and stdout carry the protocol, so after an error on
|
|
303
325
|
// either, a write's EPIPE or a read error, this transport can't reach the
|
|
304
|
-
// child even if it's still running. The handler
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
326
|
+
// child even if it's still running. The handler ends the child as close()
|
|
327
|
+
// does, SIGTERM and then SIGKILL, in the one attempt close() shares, and
|
|
328
|
+
// the exit's close names the error, so the connection handles it as any
|
|
329
|
+
// other exit. The close is the report: no 'error' is emitted, nothing is
|
|
330
|
+
// thrown, and nothing more is written meanwhile (pipeFailure). The first
|
|
331
|
+
// error starts that one attempt, and a later one, from either pipe, adds
|
|
332
|
+
// nothing to it. A child that outlives SIGKILL too stays this
|
|
333
|
+
// transport's: 'unreaped' says so, unless the host is closing it, whose
|
|
334
|
+
// close() reports the failure itself. A child that stops reading without
|
|
335
|
+
// closing its stdin raises no error: the writes are buffered, and only
|
|
336
|
+
// request timeouts bound it (McplServerConfig.requestTimeoutMs). Under
|
|
337
|
+
// Bun a child that closes its stdin isn't reported either: Bun emits no
|
|
338
|
+
// 'error' for the writes to it (checked on 1.3.10, 1.4.3 and 1.4.4's
|
|
339
|
+
// canary), so request timeouts are its only bound too.
|
|
311
340
|
const endOnPipeError = (err: Error) => {
|
|
312
|
-
this.pipeError
|
|
313
|
-
|
|
341
|
+
if (this.pipeError) return;
|
|
342
|
+
this.pipeError = err;
|
|
343
|
+
void this.terminate().catch((failure: unknown) => {
|
|
344
|
+
if (!this.closed) this.emit('unreaped', failure instanceof Error ? failure : new Error(String(failure)));
|
|
345
|
+
});
|
|
314
346
|
};
|
|
315
347
|
this.child.stdin?.on('error', endOnPipeError);
|
|
316
348
|
// On the stream itself, since close() closes readline, which takes its own
|
|
@@ -367,31 +399,89 @@ export class StdioTransport extends McplTransport {
|
|
|
367
399
|
}
|
|
368
400
|
|
|
369
401
|
writeLine(json: string): void {
|
|
402
|
+
// Nothing more goes to a child whose stdin or stdout has failed: this
|
|
403
|
+
// transport is ending it.
|
|
404
|
+
if (this.pipeError) return;
|
|
370
405
|
// stdin may be gone if the child died between the closed-check and here.
|
|
371
406
|
this.child.stdin?.write(json + '\n');
|
|
372
407
|
}
|
|
373
408
|
|
|
374
|
-
|
|
409
|
+
private closing: Promise<void> | null = null;
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Close and reap: resolves once the child has actually exited, not merely
|
|
413
|
+
* been signalled (`child.killed` is true as soon as a signal is sent). A
|
|
414
|
+
* child that outlives SIGTERM by {@link STDIO_EXIT_GRACE_MS} gets SIGKILL.
|
|
415
|
+
* If it still hasn't exited after that bound again, close rejects: an
|
|
416
|
+
* explicit cleanup failure, rather than waiting forever or claiming an
|
|
417
|
+
* exit that wasn't seen. Calls share one attempt, and so does a pipe
|
|
418
|
+
* failure that is already ending the child; a later call succeeds if the
|
|
419
|
+
* child has exited since.
|
|
420
|
+
*/
|
|
421
|
+
close(): Promise<void> {
|
|
422
|
+
if (!this.closing) {
|
|
423
|
+
this.closing = this.reap();
|
|
424
|
+
return this.closing;
|
|
425
|
+
}
|
|
426
|
+
// A later call re-checks a failed verdict: a child that has exited since
|
|
427
|
+
// is reaped now. No new signals are sent.
|
|
428
|
+
return this.closing.catch((error: unknown) => {
|
|
429
|
+
if (this.exited()) return;
|
|
430
|
+
throw error;
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private async reap(): Promise<void> {
|
|
375
435
|
this.markClosed({ reason: 'closed by host' });
|
|
376
436
|
this.rl.close();
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
437
|
+
await this.terminate();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Whether the child has exited, or never started (a spawn error). */
|
|
441
|
+
private exited(): boolean {
|
|
442
|
+
return this.child.pid === undefined || this.child.exitCode !== null || this.child.signalCode !== null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private terminating: Promise<void> | null = null;
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* End the child: SIGTERM, then SIGKILL once it has outlived SIGTERM by
|
|
449
|
+
* {@link STDIO_EXIT_GRACE_MS}. Resolves once it has exited; rejects if it
|
|
450
|
+
* still hasn't that long after SIGKILL. close() and a pipe failure share
|
|
451
|
+
* one attempt, so no second signal or timer goes out. A failed attempt is
|
|
452
|
+
* re-checked, not repeated: a child that has exited since resolves it.
|
|
453
|
+
*/
|
|
454
|
+
private terminate(): Promise<void> {
|
|
455
|
+
this.terminating ??= this.escalate();
|
|
456
|
+
return this.terminating.catch((error: unknown) => {
|
|
457
|
+
if (this.exited()) return;
|
|
458
|
+
throw error;
|
|
391
459
|
});
|
|
392
460
|
}
|
|
461
|
+
|
|
462
|
+
private async escalate(): Promise<void> {
|
|
463
|
+
const child = this.child;
|
|
464
|
+
if (this.exited()) return;
|
|
465
|
+
const exited = new Promise<void>((resolve) => child.once('exit', () => resolve()));
|
|
466
|
+
const exitWithin = (ms: number) => new Promise<boolean>((resolve) => {
|
|
467
|
+
const timer = setTimeout(() => resolve(false), ms);
|
|
468
|
+
void exited.then(() => { clearTimeout(timer); resolve(true); });
|
|
469
|
+
});
|
|
470
|
+
child.kill('SIGTERM');
|
|
471
|
+
if (await exitWithin(STDIO_EXIT_GRACE_MS)) return;
|
|
472
|
+
const named = `stdio child ${child.pid} of MCP server "${this.serverId}"`;
|
|
473
|
+
console.error(`[mcpl] ${named} ignored SIGTERM for ${STDIO_EXIT_GRACE_MS}ms — sending SIGKILL`);
|
|
474
|
+
child.kill('SIGKILL');
|
|
475
|
+
if (await exitWithin(STDIO_EXIT_GRACE_MS)) return;
|
|
476
|
+
throw new Error(
|
|
477
|
+
`${named} did not exit within ${STDIO_EXIT_GRACE_MS}ms of SIGKILL; it could not be reaped`,
|
|
478
|
+
);
|
|
479
|
+
}
|
|
393
480
|
}
|
|
394
481
|
|
|
482
|
+
/** How long a stdio child gets to exit after SIGTERM, and again after SIGKILL. */
|
|
483
|
+
const STDIO_EXIT_GRACE_MS = 2_000;
|
|
484
|
+
|
|
395
485
|
// ---------------------------------------------------------------------------
|
|
396
486
|
// WebSocket
|
|
397
487
|
// ---------------------------------------------------------------------------
|
|
@@ -399,6 +489,18 @@ export class StdioTransport extends McplTransport {
|
|
|
399
489
|
/** Timeout for the WebSocket to reach OPEN before we give up (ms). */
|
|
400
490
|
const WS_OPEN_TIMEOUT_MS = 15_000;
|
|
401
491
|
|
|
492
|
+
/** `work`, or `signal`'s reason once it aborts, whichever comes first. The
|
|
493
|
+
* listener comes off the signal when `work` settles, so a long-lived signal,
|
|
494
|
+
* such as a reconnect loop's, doesn't gather one per dial. */
|
|
495
|
+
function raceAbort<T>(work: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
496
|
+
if (signal.aborted) return Promise.reject(signal.reason);
|
|
497
|
+
return new Promise<T>((resolve, reject) => {
|
|
498
|
+
const onAbort = () => reject(signal.reason);
|
|
499
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
500
|
+
work.then(resolve, reject).finally(() => signal.removeEventListener('abort', onAbort));
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
402
504
|
export class WebSocketTransport extends McplTransport {
|
|
403
505
|
readonly kind = 'websocket' as const;
|
|
404
506
|
|
|
@@ -425,28 +527,43 @@ export class WebSocketTransport extends McplTransport {
|
|
|
425
527
|
/**
|
|
426
528
|
* Dial `config.url` and resolve once the socket is OPEN. Appends `?token=`
|
|
427
529
|
* when `config.token` is set. Rejects on connect error / timeout (and tears
|
|
428
|
-
* the socket down) so a failed dial never leaks an open handle.
|
|
530
|
+
* the socket down) so a failed dial never leaks an open handle. `signal`
|
|
531
|
+
* ends a dial whose connection is closed meanwhile, at once, rather than
|
|
532
|
+
* at the open timeout.
|
|
429
533
|
*/
|
|
430
|
-
static async open(config: McplServerConfig): Promise<WebSocketTransport> {
|
|
534
|
+
static async open(config: McplServerConfig, signal?: AbortSignal): Promise<WebSocketTransport> {
|
|
431
535
|
// Resolve the per-dial credential first: the provider runs on every
|
|
432
536
|
// connect AND reconnect, so rotation happens at the transport layer and
|
|
433
537
|
// nothing above it ever holds a credential.
|
|
434
538
|
let token: string | null = config.token ?? null;
|
|
539
|
+
const abandoned = (): Error =>
|
|
540
|
+
new Error(`MCPL server "${config.id}" WebSocket connect abandoned: the connection was closed (${redactUrl(config.url)})`);
|
|
435
541
|
if (config.accessProvider) {
|
|
542
|
+
if (signal?.aborted) throw abandoned();
|
|
436
543
|
try {
|
|
437
|
-
|
|
544
|
+
// A dial whose connection is closed meanwhile stops waiting for the
|
|
545
|
+
// provider; whatever the provider does later is ignored.
|
|
546
|
+
const provided = Promise.resolve(config.accessProvider());
|
|
547
|
+
provided.catch(() => { /* a late failure after the dial was abandoned */ });
|
|
548
|
+
token = (signal ? await raceAbort(provided, signal) : await provided) ?? token;
|
|
438
549
|
} catch (err) {
|
|
550
|
+
if (signal?.aborted) throw abandoned();
|
|
439
551
|
// Fall back to the static token (if any) rather than failing the dial
|
|
440
552
|
// outright — the server will refuse a stale credential loudly anyway.
|
|
553
|
+
// The provider's own text may quote the credential, so it isn't logged.
|
|
441
554
|
console.error(
|
|
442
|
-
`[mcpl] server "${config.id}": access provider failed (${err
|
|
555
|
+
`[mcpl] server "${config.id}": access provider failed (${describeCredentialError(err)}) — dialing with configured fallback`,
|
|
443
556
|
);
|
|
444
557
|
}
|
|
445
558
|
}
|
|
446
559
|
const url = buildWebSocketUrl(config, token);
|
|
560
|
+
// A provider's token is in this url, and the query may be re-encoded:
|
|
561
|
+
// the server's text is recorded with what the dial sent, the token in
|
|
562
|
+
// each form a token is replaced in.
|
|
563
|
+
noteDialSent(config, { url, token: token ?? undefined });
|
|
447
564
|
// Error strings travel: traces, logs, tool results the model reads. They
|
|
448
565
|
// get the endpoint, never the credential.
|
|
449
|
-
const redacted =
|
|
566
|
+
const redacted = redactUrl(config.url);
|
|
450
567
|
const ws = new WebSocket(url);
|
|
451
568
|
|
|
452
569
|
// A failed dial can emit 'error' MORE THAN ONCE on the raw socket (seen
|
|
@@ -460,31 +577,36 @@ export class WebSocketTransport extends McplTransport {
|
|
|
460
577
|
|
|
461
578
|
return new Promise<WebSocketTransport>((resolve, reject) => {
|
|
462
579
|
let settled = false;
|
|
463
|
-
const
|
|
580
|
+
const abandon = (error: Error): void => {
|
|
464
581
|
if (settled) return;
|
|
465
582
|
settled = true;
|
|
583
|
+
clearTimeout(timer);
|
|
584
|
+
signal?.removeEventListener('abort', onAbort);
|
|
466
585
|
ws.removeAllListeners();
|
|
467
586
|
ws.on('error', swallowLateErrors);
|
|
468
587
|
try { ws.terminate(); } catch { /* already gone */ }
|
|
469
|
-
reject(
|
|
588
|
+
reject(error);
|
|
589
|
+
};
|
|
590
|
+
const timer = setTimeout(() => {
|
|
591
|
+
abandon(new Error(`MCPL server "${config.id}" WebSocket connect timed out after ${WS_OPEN_TIMEOUT_MS}ms (${redacted})`));
|
|
470
592
|
}, WS_OPEN_TIMEOUT_MS);
|
|
593
|
+
const onAbort = (): void => {
|
|
594
|
+
abandon(new Error(`MCPL server "${config.id}" WebSocket connect abandoned: the connection was closed (${redacted})`));
|
|
595
|
+
};
|
|
596
|
+
if (signal?.aborted) onAbort();
|
|
597
|
+
else signal?.addEventListener('abort', onAbort, { once: true });
|
|
471
598
|
|
|
472
599
|
ws.once('open', () => {
|
|
473
600
|
if (settled) return;
|
|
474
601
|
settled = true;
|
|
475
602
|
clearTimeout(timer);
|
|
603
|
+
signal?.removeEventListener('abort', onAbort);
|
|
476
604
|
ws.removeAllListeners('error'); // the instance re-attaches its own handlers
|
|
477
605
|
resolve(new WebSocketTransport(ws, config.id));
|
|
478
606
|
});
|
|
479
607
|
|
|
480
608
|
ws.once('error', (err: Error) => {
|
|
481
|
-
|
|
482
|
-
settled = true;
|
|
483
|
-
clearTimeout(timer);
|
|
484
|
-
ws.removeAllListeners();
|
|
485
|
-
ws.on('error', swallowLateErrors);
|
|
486
|
-
try { ws.terminate(); } catch { /* already gone */ }
|
|
487
|
-
reject(new Error(`MCPL server "${config.id}" WebSocket connect failed: ${err.message} (${redacted})`));
|
|
609
|
+
abandon(new Error(`MCPL server "${config.id}" WebSocket connect failed: ${err.message} (${redacted})`));
|
|
488
610
|
});
|
|
489
611
|
});
|
|
490
612
|
}
|
|
@@ -514,8 +636,9 @@ export class WebSocketTransport extends McplTransport {
|
|
|
514
636
|
}
|
|
515
637
|
|
|
516
638
|
/**
|
|
517
|
-
* Build the dial URL: validate it's a ws(s):// URL
|
|
518
|
-
* a
|
|
639
|
+
* Build the dial URL: validate it's a ws(s):// URL the ws library will dial,
|
|
640
|
+
* which refuses one with a fragment, and append the auth token as a `token`
|
|
641
|
+
* query param when provided (preserving any existing query).
|
|
519
642
|
*/
|
|
520
643
|
export function buildWebSocketUrl(config: McplServerConfig, tokenOverride?: string | null): string {
|
|
521
644
|
if (!config.url) {
|
|
@@ -525,30 +648,45 @@ export function buildWebSocketUrl(config: McplServerConfig, tokenOverride?: stri
|
|
|
525
648
|
try {
|
|
526
649
|
parsed = new URL(config.url);
|
|
527
650
|
} catch {
|
|
528
|
-
|
|
651
|
+
// Not echoed: a url that doesn't parse can't be redacted.
|
|
652
|
+
throw new Error(`MCPL server "${config.id}": invalid WebSocket url (it isn't shown, since it may carry a credential)`);
|
|
529
653
|
}
|
|
530
654
|
if (parsed.protocol !== 'ws:' && parsed.protocol !== 'wss:') {
|
|
531
655
|
throw new Error(
|
|
532
656
|
`MCPL server "${config.id}": websocket url must be ws:// or wss://, got "${parsed.protocol}"`,
|
|
533
657
|
);
|
|
534
658
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
parsed.searchParams.set('token', token);
|
|
659
|
+
if (parsed.hash) {
|
|
660
|
+
throw new Error(`MCPL server "${config.id}": websocket url must not have a fragment, got "${parsed.hash}"`);
|
|
538
661
|
}
|
|
539
|
-
|
|
662
|
+
const token = tokenOverride !== undefined ? tokenOverride : config.token;
|
|
663
|
+
return urlAsDialed(parsed, token).toString();
|
|
540
664
|
}
|
|
541
665
|
|
|
542
666
|
// ---------------------------------------------------------------------------
|
|
543
667
|
// Factory
|
|
544
668
|
// ---------------------------------------------------------------------------
|
|
545
669
|
|
|
546
|
-
/** True when the config selects the WebSocket transport
|
|
547
|
-
*
|
|
670
|
+
/** True when the config selects the WebSocket transport, as
|
|
671
|
+
* {@link resolveServerBinding} decides it; false for any other transport and
|
|
672
|
+
* for a configuration it refuses. */
|
|
548
673
|
export function isWebSocketTransport(config: McplServerConfig): boolean {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
674
|
+
try {
|
|
675
|
+
return resolveServerBinding(config).transport === 'websocket';
|
|
676
|
+
} catch {
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/** Whether a server's binding dials it, over WebSocket or HTTP. False for a
|
|
682
|
+
* stdio launch, and for a configuration whose binding can't be resolved,
|
|
683
|
+
* which the family check refuses. */
|
|
684
|
+
function dialsNetwork(config: McplServerConfig): boolean {
|
|
685
|
+
try {
|
|
686
|
+
return resolveServerBinding(config).transport !== 'stdio';
|
|
687
|
+
} catch {
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
552
690
|
}
|
|
553
691
|
|
|
554
692
|
/** The settings of McplServerConfig whose type is boolean. */
|
|
@@ -572,25 +710,29 @@ const SERVER_CONFIG_BOOLEANS: Record<BooleanSetting, string> = {
|
|
|
572
710
|
* only `true` as on, so the quotes would hide what the value does. null counts
|
|
573
711
|
* as not set, so a generated config that writes an unset setting as null
|
|
574
712
|
* keeps working. `inheritEnv` is checked only for a stdio server, since a
|
|
575
|
-
*
|
|
576
|
-
* and the value's kind, not the value, which could be a
|
|
577
|
-
* the wrong field.
|
|
713
|
+
* network server, WebSocket or HTTP, never reads it. The error names the
|
|
714
|
+
* server, the setting and the value's kind, not the value, which could be a
|
|
715
|
+
* secret pasted into the wrong field.
|
|
578
716
|
*/
|
|
579
717
|
export function assertServerConfigBooleans(config: McplServerConfig): void {
|
|
580
718
|
for (const [setting, whatTrueDoes] of Object.entries(SERVER_CONFIG_BOOLEANS) as Array<[BooleanSetting, string]>) {
|
|
581
|
-
if (setting === 'inheritEnv' &&
|
|
719
|
+
if (setting === 'inheritEnv' && dialsNetwork(config)) continue;
|
|
582
720
|
assertBooleanSetting(`MCPL server "${config.id}" ${setting}`, config[setting], whatTrueDoes);
|
|
583
721
|
}
|
|
584
722
|
}
|
|
585
723
|
|
|
586
724
|
/**
|
|
587
|
-
* Open the transport
|
|
588
|
-
*
|
|
589
|
-
*
|
|
725
|
+
* Open the legacy transport `config` selects, as {@link resolveServerBinding}
|
|
726
|
+
* decides it: stdio for a `command`, MCPL's WebSocket binding for a ws(s)://
|
|
727
|
+
* url. A configuration it refuses, or one for the modern family (an http(s)
|
|
728
|
+
* url, or `protocol: 'modern'`), throws before anything is spawned or dialed.
|
|
729
|
+
* The returned transport is connected at the byte level but has NOT
|
|
730
|
+
* performed the MCPL handshake.
|
|
590
731
|
*/
|
|
591
|
-
export async function openTransport(config: McplServerConfig): Promise<McplTransport> {
|
|
592
|
-
|
|
593
|
-
|
|
732
|
+
export async function openTransport(config: McplServerConfig, signal?: AbortSignal): Promise<McplTransport> {
|
|
733
|
+
const binding = resolveServerBinding(config);
|
|
734
|
+
if (binding.family !== 'legacy') {
|
|
735
|
+
throw new Error(`MCP server "${config.id}" is configured for modern MCP; the legacy transports can't open it`);
|
|
594
736
|
}
|
|
595
|
-
return StdioTransport.spawn(config);
|
|
737
|
+
return binding.transport === 'websocket' ? WebSocketTransport.open(config, signal) : StdioTransport.spawn(config);
|
|
596
738
|
}
|
package/src/mcpl/types.ts
CHANGED
|
@@ -211,15 +211,22 @@ export interface McplChannelCapabilities {
|
|
|
211
211
|
/**
|
|
212
212
|
* Configuration for connecting to a single MCPL server.
|
|
213
213
|
* Provided in FrameworkConfig.mcplServers[].
|
|
214
|
+
*
|
|
215
|
+
* Treat a submitted configuration and its nested values as immutable for as
|
|
216
|
+
* long as the server uses them. Apply changed settings with
|
|
217
|
+
* `restartMcplServer(id, newConfig)`; use `accessProvider` for a network
|
|
218
|
+
* server's rotating credentials (a stdio server's changed `env` goes through
|
|
219
|
+
* a restart with the new configuration).
|
|
214
220
|
*/
|
|
215
221
|
export interface McplServerConfig {
|
|
216
222
|
/** Unique server identifier */
|
|
217
223
|
id: string;
|
|
218
224
|
|
|
219
225
|
/**
|
|
220
|
-
* Command to spawn the server process (stdio transport).
|
|
221
|
-
*
|
|
222
|
-
*
|
|
226
|
+
* Command to spawn the server process (stdio transport). Required for the
|
|
227
|
+
* stdio transport; omit it (and set `url`) for a network server, over
|
|
228
|
+
* WebSocket or HTTP. A configuration with both needs `transport` to say
|
|
229
|
+
* which one is used, and is refused without it.
|
|
223
230
|
*/
|
|
224
231
|
command?: string;
|
|
225
232
|
|
|
@@ -246,22 +253,41 @@ export interface McplServerConfig {
|
|
|
246
253
|
inheritEnv?: boolean;
|
|
247
254
|
|
|
248
255
|
/**
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
256
|
+
* URL for a network server, used instead of `command`. A configuration
|
|
257
|
+
* with both needs `transport` to say which one is used, and is refused
|
|
258
|
+
* without it. Its scheme decides the protocol family:
|
|
259
|
+
* - `ws://` or `wss://` is a legacy MCPL server over MCPL's WebSocket
|
|
260
|
+
* binding. The host dials it and runs the same newline-delimited JSON-RPC
|
|
261
|
+
* as over stdio.
|
|
262
|
+
* - `http://` or `https://` is a modern MCP (2026-07-28) server over
|
|
263
|
+
* Streamable HTTP.
|
|
253
264
|
*/
|
|
254
265
|
url?: string;
|
|
255
266
|
|
|
256
267
|
/**
|
|
257
|
-
* Transport selector.
|
|
258
|
-
*
|
|
268
|
+
* Transport selector. When it is absent, a `command` means stdio and a
|
|
269
|
+
* `url` means the transport its scheme names; with both, it is required.
|
|
270
|
+
* When it is set, it decides which of the two is used and must agree with
|
|
271
|
+
* it: `'stdio'` needs a `command`, `'websocket'` a ws(s):// url and
|
|
272
|
+
* `'http'` an http(s):// url.
|
|
273
|
+
*/
|
|
274
|
+
transport?: 'stdio' | 'websocket' | 'http';
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Protocol family for a stdio (`command`) server. Defaults to `'legacy'`,
|
|
278
|
+
* which is MCP 2024-11-05 + MCPL and today's behavior. `'modern'` speaks
|
|
279
|
+
* MCP 2026-07-28 through the SDK-backed modern engine. A network server's
|
|
280
|
+
* family comes from its url's scheme, so setting this on one is a
|
|
281
|
+
* configuration error. There is no probing between families.
|
|
259
282
|
*/
|
|
260
|
-
|
|
283
|
+
protocol?: 'legacy' | 'modern';
|
|
261
284
|
|
|
262
285
|
/**
|
|
263
|
-
* Bearer token for
|
|
264
|
-
*
|
|
286
|
+
* Bearer token for a network server. Where it travels depends on the
|
|
287
|
+
* transport: on a ws(s):// url it is appended as a `token` query parameter
|
|
288
|
+
* (`?token=…`) on connect; to an http(s):// server it is sent as an
|
|
289
|
+
* `Authorization: Bearer` header and never put in the url. A legacy stdio
|
|
290
|
+
* server ignores it, and a modern stdio server refuses it.
|
|
265
291
|
*/
|
|
266
292
|
token?: string;
|
|
267
293
|
|
|
@@ -384,12 +410,20 @@ export interface McplServerConfig {
|
|
|
384
410
|
* descriptive error, which the framework surfaces as a normal isError
|
|
385
411
|
* tool_result. Request timeouts are the only bound for a stdio server that
|
|
386
412
|
* stops reading its stdin without closing it: the host's writes are
|
|
387
|
-
* buffered, no error is raised, and the connection stays up.
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
413
|
+
* buffered, no error is raised, and the connection stays up. Under Bun they
|
|
414
|
+
* are also the only bound for one that closes its stdin, since Bun emits no
|
|
415
|
+
* 'error' event for the writes to it. requestTimeoutMs is the timeout of
|
|
416
|
+
* every request that names none; the handshake's initialize has its own.
|
|
417
|
+
* Set 0 to disable, except for channels/publish and the channels/open a
|
|
418
|
+
* delivery may send first, which keep the 60 s default: a publish's outcome
|
|
419
|
+
* is reported to the resident and recorded for its draft, so it must end.
|
|
420
|
+
* Default: 60000 (60 seconds). A value above 2^31−1 is refused: Node runs a
|
|
421
|
+
* longer timer after about 1 ms, so every request would time out at once.
|
|
422
|
+
*
|
|
423
|
+
* For a modern server it is one deadline per tool call, across every leg
|
|
424
|
+
* of the call, and must be an integer from 1 to 2^31−1. It can't be
|
|
425
|
+
* disabled: the SDK reads 0 as "time out now". At the deadline
|
|
426
|
+
* cancellation is requested and the outcome is unknown.
|
|
393
427
|
*/
|
|
394
428
|
requestTimeoutMs?: number;
|
|
395
429
|
|
|
@@ -419,7 +453,10 @@ export interface McplServerConfig {
|
|
|
419
453
|
* `toolLifecycle.*.tools`) match these full names; `enabledTools` /
|
|
420
454
|
* `disabledTools` use the bare server-side names.
|
|
421
455
|
*
|
|
422
|
-
* Must not
|
|
456
|
+
* Must not equal any module name or another server's prefix, nor meet
|
|
457
|
+
* another server's prefix: two prefixes meet when one is the other
|
|
458
|
+
* followed by `-`, or begins with the other followed by `--`, as `foo-`
|
|
459
|
+
* and `foo--bar` each meet `foo` (prefixesMeet, #265).
|
|
423
460
|
*/
|
|
424
461
|
toolPrefix?: string;
|
|
425
462
|
|
|
@@ -1075,7 +1112,15 @@ export interface ChannelsListResult {
|
|
|
1075
1112
|
* Spec Section 14.3.
|
|
1076
1113
|
*/
|
|
1077
1114
|
export interface ChannelsOpenParams {
|
|
1078
|
-
/**
|
|
1115
|
+
/**
|
|
1116
|
+
* The exact registered id to open. Without it, a server matches by `type`
|
|
1117
|
+
* and `address`, as before. A server that doesn't know the id should
|
|
1118
|
+
* refuse rather than open another channel: the host fails an open whose
|
|
1119
|
+
* answer names a different channel id, by its `channel.id` or a history
|
|
1120
|
+
* item's `channelId`, compared exactly, as the server's traffic is. When
|
|
1121
|
+
* that other channel is registered and decided closed, the host closes it
|
|
1122
|
+
* again.
|
|
1123
|
+
*/
|
|
1079
1124
|
channelId?: string;
|
|
1080
1125
|
type: string;
|
|
1081
1126
|
address?: Record<string, unknown>;
|
|
@@ -1330,6 +1375,8 @@ export interface McpToolDefinition {
|
|
|
1330
1375
|
name: string;
|
|
1331
1376
|
description?: string;
|
|
1332
1377
|
inputSchema: Record<string, unknown>;
|
|
1378
|
+
/** Server metadata for the tool (e.g. RFC-008 `mcpl/class` hints). */
|
|
1379
|
+
_meta?: Record<string, unknown>;
|
|
1333
1380
|
}
|
|
1334
1381
|
|
|
1335
1382
|
/**
|
|
@@ -36,8 +36,9 @@ export function isVisiblyEmptyContent(content: readonly unknown[] | null | undef
|
|
|
36
36
|
* block whose text isn't a string, which only a nonconformant sender writes,
|
|
37
37
|
* so the agent learns that a block was there. It is the host's word, not the
|
|
38
38
|
* sender's, so it never makes content visible (isVisiblyEmptyConversion).
|
|
39
|
+
* The other blocks the converters can't show have theirs in references.ts.
|
|
39
40
|
*/
|
|
40
|
-
export const NON_STRING_TEXT_PLACEHOLDER =
|
|
41
|
+
export const NON_STRING_TEXT_PLACEHOLDER = '[Text not shown: a text part arrived without readable text.]';
|
|
41
42
|
|
|
42
43
|
/** A wire text block whose text isn't a string, which the converter shows as
|
|
43
44
|
* NON_STRING_TEXT_PLACEHOLDER. */
|