@animalabs/agent-framework 0.30.0 → 0.31.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 +71 -8
- package/dist/src/agent.d.ts +29 -9
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +82 -17
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server-core.d.ts.map +1 -1
- package/dist/src/api/mcp-server-core.js +10 -3
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/code-execution/isolate/render.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/render.js +7 -6
- package/dist/src/code-execution/isolate/render.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +5 -4
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/framework.d.ts +129 -92
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1115 -704
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +24 -17
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +198 -144
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/recipe-reconcile.d.ts.map +1 -1
- package/dist/src/gate/recipe-reconcile.js +6 -5
- package/dist/src/gate/recipe-reconcile.js.map +1 -1
- package/dist/src/host-presentation.d.ts.map +1 -1
- package/dist/src/host-presentation.js +5 -7
- package/dist/src/host-presentation.js.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/inference-bounds.d.ts +37 -0
- package/dist/src/inference-bounds.d.ts.map +1 -0
- package/dist/src/inference-bounds.js +84 -0
- package/dist/src/inference-bounds.js.map +1 -0
- package/dist/src/line-reader.d.ts +7 -1
- package/dist/src/line-reader.d.ts.map +1 -1
- package/dist/src/line-reader.js +17 -6
- package/dist/src/line-reader.js.map +1 -1
- package/dist/src/mcpl/errors.d.ts +1 -0
- package/dist/src/mcpl/errors.d.ts.map +1 -1
- package/dist/src/mcpl/errors.js +3 -0
- package/dist/src/mcpl/errors.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +2 -2
- package/dist/src/mcpl/inbound-source.js +2 -2
- package/dist/src/mcpl/inference-router.d.ts +11 -1
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js +65 -9
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts +10 -7
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +85 -74
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +1 -1
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +45 -3
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +2 -7
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +1 -2
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +3 -3
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +14 -0
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +26 -22
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts +1 -0
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
- package/dist/src/mcpl/tool-signature.js +16 -11
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +44 -21
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +283 -64
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +17 -1
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +32 -4
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +189 -25
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
- package/dist/src/modules/discord/discord-js-client.js +24 -16
- package/dist/src/modules/discord/discord-js-client.js.map +1 -1
- package/dist/src/modules/sender-name.d.ts +5 -4
- package/dist/src/modules/sender-name.d.ts.map +1 -1
- package/dist/src/modules/sender-name.js +7 -5
- package/dist/src/modules/sender-name.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +16 -8
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +33 -12
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +6 -3
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +61 -15
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +9 -1
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +50 -10
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +11 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +19 -8
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +8 -0
- 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 +7 -0
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +7 -5
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.js +1 -9
- package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
- package/dist/src/supplied-text.d.ts +2 -0
- package/dist/src/supplied-text.d.ts.map +1 -1
- package/dist/src/supplied-text.js +16 -7
- package/dist/src/supplied-text.js.map +1 -1
- package/dist/src/tool-result-guard.js +1 -1
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +2 -20
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +175 -70
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +14 -2
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/tune-out/tools.js.map +1 -1
- package/dist/src/types/agent.d.ts +2 -1
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +54 -15
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +42 -3
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +4 -3
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agent.ts +90 -25
- package/src/api/mcp-server-core.ts +10 -3
- package/src/code-execution/isolate/render.ts +7 -6
- package/src/code-execution/isolate/scripts.ts +5 -4
- package/src/framework.ts +1145 -723
- package/src/gate/event-gate.ts +220 -145
- package/src/gate/recipe-reconcile.ts +7 -5
- package/src/host-presentation.ts +4 -7
- package/src/index.ts +1 -0
- package/src/inference-bounds.ts +111 -0
- package/src/line-reader.ts +22 -5
- package/src/mcpl/errors.ts +4 -0
- package/src/mcpl/inbound-source.ts +2 -2
- package/src/mcpl/inference-router.ts +67 -10
- package/src/mcpl/modern-connection.ts +83 -68
- package/src/mcpl/protocol-family.ts +37 -4
- package/src/mcpl/push-coalescer.ts +2 -7
- package/src/mcpl/references.ts +3 -4
- package/src/mcpl/server-connection.ts +30 -21
- package/src/mcpl/tool-signature.ts +18 -11
- package/src/mcpl/transport.ts +261 -60
- package/src/mcpl/types.ts +18 -1
- package/src/module-registry.ts +178 -25
- package/src/modules/discord/discord-js-client.ts +33 -29
- package/src/modules/sender-name.ts +8 -5
- package/src/modules/workspace/disk-agreement.ts +39 -15
- package/src/modules/workspace/index.ts +65 -14
- package/src/modules/workspace/observe.ts +51 -10
- package/src/modules/workspace/reconcile.ts +22 -8
- package/src/modules/workspace/types.ts +5 -0
- package/src/modules/workspace/watcher.ts +8 -6
- package/src/recovery/discord-awareness-outbox.ts +1 -11
- package/src/supplied-text.ts +17 -6
- package/src/tool-result-guard.ts +1 -1
- package/src/tune-out/coordinator.ts +187 -85
- package/src/tune-out/tools.ts +14 -2
- package/src/types/agent.ts +2 -1
- package/src/types/framework.ts +55 -15
- package/src/types/module.ts +42 -3
- package/src/types/trace.ts +4 -3
|
@@ -66,8 +66,8 @@ import {
|
|
|
66
66
|
type TransportSendOptions,
|
|
67
67
|
} from '@modelcontextprotocol/client';
|
|
68
68
|
|
|
69
|
-
import { StdioTransport, type McplTransport } from './transport.js';
|
|
70
|
-
import { McplProtocolVersionError, McplRequestError, McplUnreapedLaunchError } from './server-connection.js';
|
|
69
|
+
import { HeldStderr, STDERR_END_WAIT_MS, StdioTransport, type McplTransport, type TransportCloseInfo } from './transport.js';
|
|
70
|
+
import { McplProtocolVersionError, McplRequestError, McplUnreapedLaunchError, stderrTailOf, withStderr } from './server-connection.js';
|
|
71
71
|
import { LEGACY_MCP_PROTOCOL_VERSION, MAX_TIMER_MS, MODERN_MCP_PROTOCOL_VERSION, checkServerConfig, describeCredentialError, logDroppedLine, maskedError, noteDialSent, redactServerText, redactedData, serverTextRedactor, type ServerTextRedactor } from './protocol-family.js';
|
|
72
72
|
import type { McplServerConfig, McpToolDefinition } from './types.js';
|
|
73
73
|
import { boundedForLog } from './errors.js';
|
|
@@ -79,8 +79,6 @@ const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
|
79
79
|
const CONNECT_TIMEOUT_MS = 30_000;
|
|
80
80
|
/** Inventory listing has its own bound: it is host work, not a resident's call. */
|
|
81
81
|
const LIST_TIMEOUT_MS = 30_000;
|
|
82
|
-
/** Startup stderr kept for the first listener. */
|
|
83
|
-
const STDERR_BACKLOG_LINES = 200;
|
|
84
82
|
|
|
85
83
|
/** A modern tool result as the framework consumes it. `structuredContent` is
|
|
86
84
|
* kept by presence: `false`, `0` and `null` are values, not absence. */
|
|
@@ -113,18 +111,12 @@ interface WireRecord {
|
|
|
113
111
|
continued: boolean;
|
|
114
112
|
}
|
|
115
113
|
|
|
116
|
-
/**
|
|
117
|
-
* A send that Connectome's own transport refused before writing anything (its
|
|
118
|
-
* stdio line is closed): nothing of that request reached the server. `pipe`:
|
|
119
|
-
* the child's stdin or stdout had failed, so this host can no longer write to
|
|
120
|
-
* that server at all.
|
|
121
|
-
*/
|
|
122
114
|
class NotWrittenError extends Error {
|
|
123
|
-
readonly
|
|
124
|
-
constructor(message: string,
|
|
115
|
+
readonly stdioStopped: boolean;
|
|
116
|
+
constructor(message: string, stdioStopped = false) {
|
|
125
117
|
super(message);
|
|
126
118
|
this.name = 'NotWrittenError';
|
|
127
|
-
this.
|
|
119
|
+
this.stdioStopped = stdioStopped;
|
|
128
120
|
}
|
|
129
121
|
}
|
|
130
122
|
|
|
@@ -309,6 +301,9 @@ class SpawnerStdioTransport implements Transport {
|
|
|
309
301
|
onerror?: (error: Error) => void;
|
|
310
302
|
onmessage?: <T extends JSONRPCMessage>(message: T, extra?: MessageExtraInfo) => void;
|
|
311
303
|
private line: McplTransport | null = null;
|
|
304
|
+
/** How the child ended, once its line has closed: the line's reason, and
|
|
305
|
+
* the child's exit code or signal. */
|
|
306
|
+
closeInfo: TransportCloseInfo | undefined;
|
|
312
307
|
/** The spawn start() awaits, so a close() made meanwhile closes its child. */
|
|
313
308
|
private spawning: Promise<McplTransport> | null = null;
|
|
314
309
|
|
|
@@ -318,9 +313,6 @@ class SpawnerStdioTransport implements Transport {
|
|
|
318
313
|
constructor(
|
|
319
314
|
private readonly config: McplServerConfig,
|
|
320
315
|
private readonly onStderr: (line: string) => void,
|
|
321
|
-
/** The child's pipe failed and it outlived SIGKILL too, so the line
|
|
322
|
-
* stays open, owning it (the line's 'unreaped'): `reason` names the
|
|
323
|
-
* pipe's error, and `failure` is the reap's. */
|
|
324
316
|
private readonly onUnreaped: (reason: string, failure: Error) => void,
|
|
325
317
|
) {
|
|
326
318
|
this.recorded = serverTextRedactor(config);
|
|
@@ -357,22 +349,31 @@ class SpawnerStdioTransport implements Transport {
|
|
|
357
349
|
});
|
|
358
350
|
line.on('stderr', (text: string) => this.onStderr(text));
|
|
359
351
|
line.on('error', (error: Error) => this.onerror?.(error));
|
|
360
|
-
line.on('close', () =>
|
|
361
|
-
|
|
352
|
+
line.on('close', (info: TransportCloseInfo) => {
|
|
353
|
+
this.closeInfo = info;
|
|
354
|
+
this.onclose?.();
|
|
355
|
+
});
|
|
356
|
+
line.on('unreaped', (failure: Error) => this.onUnreaped(`this host stopped using the server's stdio (${line.stdioStopCause?.message ?? 'an error'})`, failure));
|
|
362
357
|
}
|
|
363
358
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
359
|
+
get stdioStopCause(): Error | undefined {
|
|
360
|
+
return this.line?.stdioStopCause;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** The last of the child's stderr, redacted, once the stream has ended or
|
|
364
|
+
* STDERR_END_WAIT_MS has passed, as the legacy handshake waits before it
|
|
365
|
+
* refuses; '' when there's no child or it wrote nothing. */
|
|
366
|
+
async stderrTail(): Promise<string> {
|
|
367
|
+
const line = this.line;
|
|
368
|
+
if (!line) return '';
|
|
369
|
+
await line.stderrEnded(STDERR_END_WAIT_MS);
|
|
370
|
+
return line.stderrTailRedacted(serverTextRedactor(this.config)).trimEnd();
|
|
368
371
|
}
|
|
369
372
|
|
|
370
373
|
async send(message: JSONRPCMessage): Promise<void> {
|
|
371
374
|
if (!this.line || this.line.closed) throw new NotWrittenError(`MCP server "${this.config.id}" stdio transport is closed`);
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const failed = this.pipeFailure;
|
|
375
|
-
if (failed) throw new NotWrittenError(`MCP server "${this.config.id}" stdio pipe failed with ${failed.message}`, true);
|
|
375
|
+
const stopped = this.stdioStopCause;
|
|
376
|
+
if (stopped) throw new NotWrittenError(`MCP server "${this.config.id}": this host stopped using the server's stdio (${stopped.message})`, true);
|
|
376
377
|
this.line.writeLine(JSON.stringify(message));
|
|
377
378
|
}
|
|
378
379
|
|
|
@@ -460,10 +461,7 @@ export class ModernMcpConnection extends EventEmitter {
|
|
|
460
461
|
/** A lost session's reap in flight: the reconnect follows it, and
|
|
461
462
|
* close() waits for it. */
|
|
462
463
|
private losing: { session: Session; done: Promise<void> } | null = null;
|
|
463
|
-
|
|
464
|
-
* connect() returns), delivered to the first listener. Bounded: the
|
|
465
|
-
* newest lines are kept. */
|
|
466
|
-
private stderrBacklog: string[] = [];
|
|
464
|
+
private readonly heldStderr = new HeldStderr();
|
|
467
465
|
private redactor?: ServerTextRedactor;
|
|
468
466
|
|
|
469
467
|
/** The server's text with its configuration's credentials replaced, for
|
|
@@ -489,9 +487,8 @@ export class ModernMcpConnection extends EventEmitter {
|
|
|
489
487
|
if (this.listenerCount('error') === 1) console.error(`[mcp] ${this.id}: ${redactServerText(error.message, this.config)}`);
|
|
490
488
|
});
|
|
491
489
|
this.on('newListener', (event: string | symbol) => {
|
|
492
|
-
if (event !== 'stderr' || this.
|
|
493
|
-
const pending = this.
|
|
494
|
-
this.stderrBacklog = [];
|
|
490
|
+
if (event !== 'stderr' || this.heldStderr.empty) return;
|
|
491
|
+
const pending = this.heldStderr.take();
|
|
495
492
|
// After the listener is added (newListener fires just before).
|
|
496
493
|
queueMicrotask(() => { for (const line of pending) this.emit('stderr', { line }); });
|
|
497
494
|
});
|
|
@@ -502,8 +499,7 @@ export class ModernMcpConnection extends EventEmitter {
|
|
|
502
499
|
this.emit('stderr', { line });
|
|
503
500
|
return;
|
|
504
501
|
}
|
|
505
|
-
this.
|
|
506
|
-
if (this.stderrBacklog.length > STDERR_BACKLOG_LINES) this.stderrBacklog.shift();
|
|
502
|
+
this.heldStderr.hold(line);
|
|
507
503
|
}
|
|
508
504
|
|
|
509
505
|
/** The per-call deadline: `requestTimeoutMs`, validated positive. */
|
|
@@ -520,6 +516,14 @@ export class ModernMcpConnection extends EventEmitter {
|
|
|
520
516
|
return this.reconnectEnabled && !this.closedByHost && this.unreaped.length === 0;
|
|
521
517
|
}
|
|
522
518
|
|
|
519
|
+
/**
|
|
520
|
+
* Stop reconnecting, closing nothing, as the legacy engine's does.
|
|
521
|
+
* @internal For the framework's stop(); not a host API.
|
|
522
|
+
*/
|
|
523
|
+
stopReconnecting(): void {
|
|
524
|
+
this.reconnectEnabled = false;
|
|
525
|
+
}
|
|
526
|
+
|
|
523
527
|
/** Whether close() has been called: closed for good, or closing. A
|
|
524
528
|
* connection lost on its own is not closed in this sense. */
|
|
525
529
|
get isClosed(): boolean {
|
|
@@ -610,12 +614,14 @@ export class ModernMcpConnection extends EventEmitter {
|
|
|
610
614
|
// `cause`, can carry it whole (maskedError). The protocol refusal
|
|
611
615
|
// above was built from redacted words.
|
|
612
616
|
const masked = maskedError(error, this.recorded);
|
|
617
|
+
const stderrTail = stderrTailOf(error);
|
|
618
|
+
if (stderrTail !== undefined) Object.assign(masked, { stderrTail });
|
|
613
619
|
// A launch that couldn't be reaped halts reconnecting, so no retry
|
|
614
620
|
// will come: the first connect fails, as one without reconnect does.
|
|
615
621
|
if (!this.reconnectEnabled || this.holdsUnreapedChild) throw masked;
|
|
616
|
-
console.error(`MCP server "${this.id}" initial connect failed, will retry
|
|
622
|
+
console.error(`MCP server "${this.id}" initial connect failed, will retry: ${masked.message}${withStderr(masked, this.recorded)}`);
|
|
617
623
|
this.reconnectAttempts = 1;
|
|
618
|
-
this.emit('connect-failed', { error: (error as Error).message, attempt: 0 });
|
|
624
|
+
this.emit('connect-failed', { error: (error as Error).message, ...(stderrTail === undefined ? {} : { stderrTail }), attempt: 0 });
|
|
619
625
|
this.scheduleReconnect();
|
|
620
626
|
}
|
|
621
627
|
}
|
|
@@ -656,11 +662,6 @@ export class ModernMcpConnection extends EventEmitter {
|
|
|
656
662
|
? new SpawnerStdioTransport(
|
|
657
663
|
this.config,
|
|
658
664
|
(line) => { if (this.isCurrent(generation)) this.deliverStderr(line); },
|
|
659
|
-
// A child whose pipe failed and that outlived SIGKILL: a loss whose
|
|
660
|
-
// launch is already known unreaped, so it's held as the loss is
|
|
661
|
-
// handled (handleLost). During a connect there is no session to lose
|
|
662
|
-
// yet: a connect whose pipe has failed fails, whether or not the SDK
|
|
663
|
-
// resolves it (establish), and its cleanup holds the launch.
|
|
664
665
|
(reason, failure) => { if (this.isCurrent(generation)) this.handleLost(generation, reason, failure); },
|
|
665
666
|
)
|
|
666
667
|
: null;
|
|
@@ -697,7 +698,9 @@ export class ModernMcpConnection extends EventEmitter {
|
|
|
697
698
|
// Marked before the session is adopted too: a close during the connect
|
|
698
699
|
// makes it a failed connect (see establish).
|
|
699
700
|
session.lost = true;
|
|
700
|
-
|
|
701
|
+
// Over stdio, how its child ended, as legacy's loss carries it.
|
|
702
|
+
const ended = session.stdio?.closeInfo;
|
|
703
|
+
if (this.isCurrent(generation)) this.handleLost(generation, ended?.reason ?? 'transport closed', undefined, ended);
|
|
701
704
|
};
|
|
702
705
|
|
|
703
706
|
// `done` settles only once this connect's own cleanup has finished, on
|
|
@@ -713,6 +716,13 @@ export class ModernMcpConnection extends EventEmitter {
|
|
|
713
716
|
}
|
|
714
717
|
}
|
|
715
718
|
|
|
719
|
+
/** `error` carrying the session's stderr tail as `stderrTail`, as a
|
|
720
|
+
* legacy connect's failure does, when the child wrote any. */
|
|
721
|
+
private static async withStderrTail(session: Session, error: unknown): Promise<unknown> {
|
|
722
|
+
const tail = await session.stdio?.stderrTail();
|
|
723
|
+
return tail && error instanceof Error ? Object.assign(error, { stderrTail: tail }) : error;
|
|
724
|
+
}
|
|
725
|
+
|
|
716
726
|
/** Connect one generation's client, and adopt it if it's still current. */
|
|
717
727
|
private async establish(session: Session): Promise<void> {
|
|
718
728
|
try {
|
|
@@ -720,7 +730,7 @@ export class ModernMcpConnection extends EventEmitter {
|
|
|
720
730
|
} catch (error) {
|
|
721
731
|
const failure = await ModernMcpConnection.closeSession(session);
|
|
722
732
|
if (failure) this.holdUnreaped(session, failure);
|
|
723
|
-
throw this.versionVerdict(error) ?? error;
|
|
733
|
+
throw await ModernMcpConnection.withStderrTail(session, this.versionVerdict(error) ?? error);
|
|
724
734
|
}
|
|
725
735
|
if (!this.isCurrent(session.generation)) {
|
|
726
736
|
// Superseded or closed while connecting: this generation never serves.
|
|
@@ -729,22 +739,22 @@ export class ModernMcpConnection extends EventEmitter {
|
|
|
729
739
|
return;
|
|
730
740
|
}
|
|
731
741
|
// The transport closed while the client connected, such as a child that
|
|
732
|
-
// exited at the connect-time list-change subscribe, or
|
|
733
|
-
//
|
|
734
|
-
// reports the subscribe's failure only as an error, and
|
|
735
|
-
// connect anyway (R1-2). Adopted, the session would read as
|
|
736
|
-
// with nothing it can write to behind it, and from a child that
|
|
737
|
-
// SIGKILL, whose 'unreaped' came while there was no session to
|
|
738
|
-
// later close would come to reconnect it. So the connect failed:
|
|
739
|
-
// close reaps the child, and a child it can't reap is held.
|
|
740
|
-
//
|
|
741
|
-
const
|
|
742
|
-
if (session.lost ||
|
|
742
|
+
// exited at the connect-time list-change subscribe, or this host stopped
|
|
743
|
+
// using the child's stdio, such as after that subscribe's write met
|
|
744
|
+
// EPIPE: the SDK reports the subscribe's failure only as an error, and
|
|
745
|
+
// resolves the connect anyway (R1-2). Adopted, the session would read as
|
|
746
|
+
// connected with nothing it can write to behind it, and from a child that
|
|
747
|
+
// outlived SIGKILL, whose 'unreaped' came while there was no session to
|
|
748
|
+
// lose, no later close would come to reconnect it. So the connect failed:
|
|
749
|
+
// its close reaps the child, and a child it can't reap is held. The
|
|
750
|
+
// stop's cause is named whether or not the child has exited by now.
|
|
751
|
+
const stopped = session.stdio?.stdioStopCause;
|
|
752
|
+
if (session.lost || stopped) {
|
|
743
753
|
const failure = await ModernMcpConnection.closeSession(session);
|
|
744
754
|
if (failure) this.holdUnreaped(session, failure);
|
|
745
|
-
throw new Error(
|
|
746
|
-
? `MCP server "${this.id}" stdio
|
|
747
|
-
: `MCP server "${this.id}" closed its connection while connecting`);
|
|
755
|
+
throw await ModernMcpConnection.withStderrTail(session, new Error(stopped
|
|
756
|
+
? `MCP server "${this.id}": this host stopped using the server's stdio while connecting (${stopped.message})`
|
|
757
|
+
: `MCP server "${this.id}" closed its connection while connecting`));
|
|
748
758
|
}
|
|
749
759
|
this.session = session;
|
|
750
760
|
this.protocolVersion = MODERN_MCP_PROTOCOL_VERSION;
|
|
@@ -847,10 +857,8 @@ export class ModernMcpConnection extends EventEmitter {
|
|
|
847
857
|
}
|
|
848
858
|
}
|
|
849
859
|
|
|
850
|
-
/** The transport went away (child exit, or a closed HTTP client).
|
|
851
|
-
|
|
852
|
-
* launch is already known unreaped. */
|
|
853
|
-
private handleLost(generation: number, reason: string, unreaped?: Error): void {
|
|
860
|
+
/** The transport went away (child exit, or a closed HTTP client). */
|
|
861
|
+
private handleLost(generation: number, reason: string, unreaped?: Error, ended?: TransportCloseInfo): void {
|
|
854
862
|
const session = this.session;
|
|
855
863
|
if (session?.generation !== generation) return;
|
|
856
864
|
this.session = null;
|
|
@@ -886,7 +894,7 @@ export class ModernMcpConnection extends EventEmitter {
|
|
|
886
894
|
// skip neither 'close' nor the report.
|
|
887
895
|
if (unreaped) this.holdUnreaped(session, unreaped, false);
|
|
888
896
|
try {
|
|
889
|
-
this.emit('close', { reason });
|
|
897
|
+
this.emit('close', { reason, code: ended?.code ?? null, signal: ended?.signal ?? null, pipeFailed: ended?.pipeFailed === true });
|
|
890
898
|
} finally {
|
|
891
899
|
if (unreaped) this.reportUnreaped(unreaped);
|
|
892
900
|
}
|
|
@@ -920,19 +928,26 @@ export class ModernMcpConnection extends EventEmitter {
|
|
|
920
928
|
} catch (error) {
|
|
921
929
|
if (this.closedByHost) return;
|
|
922
930
|
this.reconnectAttempts = attempt + 1;
|
|
931
|
+
const stderrTail = stderrTailOf(error);
|
|
932
|
+
const failed = (message: string, permanent?: true) => this.emit('reconnect-failed', {
|
|
933
|
+
error: message,
|
|
934
|
+
...(stderrTail === undefined ? {} : { stderrTail }),
|
|
935
|
+
attempt,
|
|
936
|
+
...(permanent ? { permanent } : {}),
|
|
937
|
+
});
|
|
923
938
|
const halted = this.unreapedFailure();
|
|
924
939
|
if (halted) {
|
|
925
|
-
|
|
940
|
+
failed(`${(error as Error).message}; ${halted}`, true);
|
|
926
941
|
return;
|
|
927
942
|
}
|
|
928
943
|
// No common revision: every further attempt would hear the same
|
|
929
944
|
// answer. Stop; only the configuration can fix it.
|
|
930
945
|
if (error instanceof McplProtocolVersionError) {
|
|
931
946
|
this.reconnectEnabled = false;
|
|
932
|
-
|
|
947
|
+
failed(error.message, true);
|
|
933
948
|
return;
|
|
934
949
|
}
|
|
935
|
-
|
|
950
|
+
failed((error as Error).message);
|
|
936
951
|
this.scheduleReconnect();
|
|
937
952
|
}
|
|
938
953
|
}
|
|
@@ -1168,9 +1183,9 @@ export class ModernMcpConnection extends EventEmitter {
|
|
|
1168
1183
|
: `${said}, so whether the request took effect is unknown. Check its effect before trying again.`;
|
|
1169
1184
|
if (!record.handedOff) {
|
|
1170
1185
|
log(`${what} not sent: ${logDetail(message)}`);
|
|
1171
|
-
const
|
|
1186
|
+
const stdioStopped = error instanceof NotWrittenError && error.stdioStopped;
|
|
1172
1187
|
return new McplRequestError(
|
|
1173
|
-
|
|
1188
|
+
stdioStopped
|
|
1174
1189
|
? `The request wasn't sent: this host can no longer write to ${server}, so nothing ran. ${this.retryText()}`
|
|
1175
1190
|
// Before the wire, the cause can pass (the stdio line closing as
|
|
1176
1191
|
// the connection is lost) or stay (a tool's outputSchema the SDK
|
|
@@ -695,7 +695,7 @@ export function serverConfigProblems(config: McplServerConfig): string[] {
|
|
|
695
695
|
} catch (error) {
|
|
696
696
|
return [(error as Error).message];
|
|
697
697
|
}
|
|
698
|
-
const problems: string[] =
|
|
698
|
+
const problems: string[] = credentialRequiredProblems(config);
|
|
699
699
|
if (binding.transport === 'http') {
|
|
700
700
|
// fetch refuses a url with userinfo, and its error quotes the whole url,
|
|
701
701
|
// password included, into the connect failure's log line, trace and
|
|
@@ -760,6 +760,38 @@ export function serverConfigProblems(config: McplServerConfig): string[] {
|
|
|
760
760
|
return problems;
|
|
761
761
|
}
|
|
762
762
|
|
|
763
|
+
/** Strict-only admission, also applied to a captured policy at dial time.
|
|
764
|
+
* Nothing supplied in an invalid setting is quoted. Runtime providers are
|
|
765
|
+
* host-attached, so their presence is checked by the dial, not here. */
|
|
766
|
+
function credentialRequiredProblems(config: McplServerConfig): string[] {
|
|
767
|
+
const flag: unknown = config.credentialRequired;
|
|
768
|
+
if (flag === undefined || flag === null || flag === false) return [];
|
|
769
|
+
const problem = (why: string) => [`MCP server "${config.id}": credentialRequired ${why}`];
|
|
770
|
+
if (flag !== true) return problem('must be a boolean');
|
|
771
|
+
let binding: ServerBinding;
|
|
772
|
+
try { binding = resolveServerBinding(config); }
|
|
773
|
+
catch { return problem('requires a valid WebSocket binding'); }
|
|
774
|
+
if (binding.transport !== 'websocket' || binding.family !== 'legacy') {
|
|
775
|
+
return problem('applies only to a legacy ws(s):// server, not HTTP, stdio or modern servers');
|
|
776
|
+
}
|
|
777
|
+
if (config.token !== undefined && config.token !== null) return problem('forbids a static token, including an empty token');
|
|
778
|
+
if (config.tokenTransport !== 'header') return problem("requires tokenTransport 'header'");
|
|
779
|
+
const url = new URL(config.url!);
|
|
780
|
+
if (url.hash) return problem('forbids a URL fragment');
|
|
781
|
+
if (url.protocol === 'ws:') {
|
|
782
|
+
if (!isLoopbackHost(url.hostname)) return problem('requires TLS outside loopback');
|
|
783
|
+
const name = url.hostname.toLowerCase();
|
|
784
|
+
const host = name.replace(/\.$/, '');
|
|
785
|
+
if (host.endsWith('.localhost') || name === 'localhost.') {
|
|
786
|
+
return problem('requires TLS for a localhost name other than localhost itself, which a resolver can send off this host; use localhost or a loopback address');
|
|
787
|
+
}
|
|
788
|
+
if (host === '0.0.0.0' || host === '[::]') {
|
|
789
|
+
return problem('requires TLS for the unspecified address, which is not a loopback address; use 127.0.0.1 or [::1]');
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return [];
|
|
793
|
+
}
|
|
794
|
+
|
|
763
795
|
/**
|
|
764
796
|
* What's wrong with a WebSocket server's `tokenTransport`, as a message, or
|
|
765
797
|
* undefined: a value that is neither mode, or `'header'` with a url that
|
|
@@ -770,18 +802,19 @@ export function serverConfigProblems(config: McplServerConfig): string[] {
|
|
|
770
802
|
* The refusal names the value's kind, never the value, which could be a
|
|
771
803
|
* credential pasted into the wrong field, and quotes nothing of the url.
|
|
772
804
|
*/
|
|
773
|
-
export function tokenTransportProblem(config: Pick<McplServerConfig, 'id' | 'tokenTransport'>, url: URL): string | undefined {
|
|
805
|
+
export function tokenTransportProblem(config: Pick<McplServerConfig, 'id' | 'tokenTransport' | 'credentialRequired'>, url: URL): string | undefined {
|
|
774
806
|
// A configuration read from JSON can hold anything here.
|
|
775
807
|
const mode: unknown = config.tokenTransport;
|
|
776
808
|
if (mode === undefined || mode === null || mode === 'query') return undefined;
|
|
777
809
|
if (mode !== 'header') {
|
|
778
810
|
return `MCP server "${config.id}": tokenTransport must be 'query' or 'header', not ${typeof mode === 'string' ? 'another string' : kindOf(mode)}`;
|
|
779
811
|
}
|
|
812
|
+
const strict = config.credentialRequired === true;
|
|
780
813
|
if (url.searchParams.has('token')) {
|
|
781
|
-
return `MCP server "${config.id}": with tokenTransport 'header' the url must not carry a "token" query parameter, which would still put a credential in it; give it as "token" or "accessProvider"`;
|
|
814
|
+
return `MCP server "${config.id}": with tokenTransport 'header' the url must not carry a "token" query parameter, which would still put a credential in it; give it as ${strict ? '"accessProvider"' : '"token" or "accessProvider"'}`;
|
|
782
815
|
}
|
|
783
816
|
if (url.username || url.password) {
|
|
784
|
-
return `MCP server "${config.id}": with tokenTransport 'header' the url must not carry userinfo (user:password@), since the Bearer header replaces the userinfo's Basic credential whenever it carries a credential; leave out "tokenTransport" to send the userinfo as before, or give the credential as "token" or "accessProvider"`;
|
|
817
|
+
return `MCP server "${config.id}": with tokenTransport 'header' the url must not carry userinfo (user:password@), since the Bearer header replaces the userinfo's Basic credential whenever it carries a credential; ${strict ? 'give the credential as "accessProvider"' : 'leave out "tokenTransport" to send the userinfo as before, or give the credential as "token" or "accessProvider"'}`;
|
|
785
818
|
}
|
|
786
819
|
return undefined;
|
|
787
820
|
}
|
|
@@ -295,18 +295,13 @@ export interface CoalescingReceiptRecord {
|
|
|
295
295
|
|
|
296
296
|
export interface CoalescerHost<E> {
|
|
297
297
|
/** True iff the placement still exists and, as far as the host can tell,
|
|
298
|
-
* no model request has included it.
|
|
299
|
-
* message injected into a stream that didn't say whether it carries
|
|
300
|
-
* injections, as no membrane 0.5.x stream does, counts as unread though
|
|
301
|
-
* a request may have included it. */
|
|
298
|
+
* no model request has included it. */
|
|
302
299
|
isUnread(placement: CoalescingPlacement): boolean;
|
|
303
300
|
/** True iff nothing may have shown the placement to the agent: it is still
|
|
304
301
|
* in the deferred queue and known not to be stored already, or it is
|
|
305
302
|
* stored where no committing compile has begun to assemble it and no tool
|
|
306
303
|
* boundary has injected past it. Unlike
|
|
307
|
-
* isUnread, a compile counts from its start
|
|
308
|
-
* whether or not its stream says it carries injected messages: a doubt
|
|
309
|
-
* counts as shown. */
|
|
304
|
+
* isUnread, a compile counts from its start: a doubt counts as shown. */
|
|
310
305
|
isUnshown(placement: CoalescingPlacement): boolean;
|
|
311
306
|
/** False when the unread occurrence could not be removed (it may still be read). */
|
|
312
307
|
remove(placement: CoalescingPlacement): boolean;
|
package/src/mcpl/references.ts
CHANGED
|
@@ -50,7 +50,6 @@ export interface ReferenceTestimony {
|
|
|
50
50
|
name?: string;
|
|
51
51
|
disposition?: ReferenceDisposition;
|
|
52
52
|
rejectedFields: string[];
|
|
53
|
-
truncatedFields: string[];
|
|
54
53
|
/** Set when the testimony is an MCP resource_link's, which claims no
|
|
55
54
|
* digest, expiry or disposition. A link is registered but never fetched
|
|
56
55
|
* on the side, and its testimony never replaces a record's (register). */
|
|
@@ -182,8 +181,8 @@ function parseReferenceFields(block: Record<string, unknown>): BlockClassificati
|
|
|
182
181
|
if (block.uri.length > REFERENCE_LIMITS.uri) {
|
|
183
182
|
return { kind: 'invalid', shown: REFERENCE_LINK_TOO_LONG_TEXT };
|
|
184
183
|
}
|
|
185
|
-
const out = { rejected: [] as string[]
|
|
186
|
-
const t: ReferenceTestimony = { uri: block.uri, rejectedFields: out.rejected
|
|
184
|
+
const out = { rejected: [] as string[] };
|
|
185
|
+
const t: ReferenceTestimony = { uri: block.uri, rejectedFields: out.rejected };
|
|
187
186
|
t.mimeType = takeString(block.mimeType, 'mimeType', out);
|
|
188
187
|
t.name = takeString(block.name, 'name', out);
|
|
189
188
|
t.expiresAt = takeString(block.expiresAt, 'expiresAt', out);
|
|
@@ -835,7 +834,7 @@ export class ReferenceRegistry {
|
|
|
835
834
|
* entry for the id, if any, is in force again; with none, a stub reads the
|
|
836
835
|
* id's references as an unknown server's. After a removal, that framework's
|
|
837
836
|
* fetch refuses them too. A stopped framework keeps its configs, so its
|
|
838
|
-
* fetcher
|
|
837
|
+
* fetcher can still dial them. */
|
|
839
838
|
forgetServerAddress(serverId: string, owner: object): void {
|
|
840
839
|
const entries = (this.addresses.get(serverId) ?? []).filter((e) => e.owner !== owner);
|
|
841
840
|
if (entries.length > 0) this.addresses.set(serverId, entries);
|
|
@@ -79,6 +79,9 @@ export interface McplConnectionCloseInfo {
|
|
|
79
79
|
/** True when `reason` is the peer's own text, a WebSocket server's close
|
|
80
80
|
* reason, rather than the transport's or the host's words. */
|
|
81
81
|
reasonFromPeer?: boolean;
|
|
82
|
+
/** True when the stdio transport ended the child itself, after a pipe
|
|
83
|
+
* error or a line on its stdout longer than the bound. */
|
|
84
|
+
pipeFailed?: boolean;
|
|
82
85
|
/** True when the host closed the link (`close()` / `reconnectAfterFailure()`),
|
|
83
86
|
* false when the transport dropped unexpectedly. */
|
|
84
87
|
byHost: boolean;
|
|
@@ -454,6 +457,8 @@ export class McplServerConnection extends EventEmitter {
|
|
|
454
457
|
* the epoch their opening went to, never across a reconnect. */
|
|
455
458
|
transportEpoch = 0;
|
|
456
459
|
|
|
460
|
+
private transportEnded = new AbortController();
|
|
461
|
+
|
|
457
462
|
/**
|
|
458
463
|
* RFC-014: the server's `toolHints`, `toolsets` and `surface` members as it
|
|
459
464
|
* advertised them at the last handshake (toolsetDeclarationsOf), or null.
|
|
@@ -531,6 +536,8 @@ export class McplServerConnection extends EventEmitter {
|
|
|
531
536
|
this.grant = CapabilityGrant.empty();
|
|
532
537
|
this.policyEstablished = false;
|
|
533
538
|
this.transportEpoch++;
|
|
539
|
+
this.transportEnded.abort();
|
|
540
|
+
this.transportEnded = new AbortController();
|
|
534
541
|
// What the server sent that is still held came on the transport that is
|
|
535
542
|
// now gone. What it said about its own process is dropped: its requests
|
|
536
543
|
// wait on answers that can no longer reach whoever asked (an inference
|
|
@@ -726,6 +733,20 @@ export class McplServerConnection extends EventEmitter {
|
|
|
726
733
|
return this.reconnectEnabled;
|
|
727
734
|
}
|
|
728
735
|
|
|
736
|
+
/**
|
|
737
|
+
* Stop reconnecting, closing nothing: a retry already scheduled starts
|
|
738
|
+
* nothing when its timer fires, none is scheduled after, and an attempt
|
|
739
|
+
* under way is aborted, as close() aborts it, so one still waiting on its
|
|
740
|
+
* access provider never dials and a child it spawned is reaped. The host's
|
|
741
|
+
* stop() does this first, so nothing is launched or dialed while it shuts
|
|
742
|
+
* down; close() clears the timer.
|
|
743
|
+
* @internal For the framework's stop(); not a host API.
|
|
744
|
+
*/
|
|
745
|
+
stopReconnecting(): void {
|
|
746
|
+
this.reconnectEnabled = false;
|
|
747
|
+
this.reconnecting?.abort.abort();
|
|
748
|
+
}
|
|
749
|
+
|
|
729
750
|
/** Whether a child this connection launched couldn't be confirmed exited.
|
|
730
751
|
* While it's true the connection owns that child, so whatever registered
|
|
731
752
|
* it keeps its id held; close() retries the reap. */
|
|
@@ -1417,7 +1438,7 @@ export class McplServerConnection extends EventEmitter {
|
|
|
1417
1438
|
transport.writeLine(JSON.stringify(initRequest));
|
|
1418
1439
|
});
|
|
1419
1440
|
|
|
1420
|
-
// The transport has
|
|
1441
|
+
// The transport has stopped using the child's stdio before the
|
|
1421
1442
|
// handshake could hand it on, such as when this host's answer to a ping
|
|
1422
1443
|
// the server sent met EPIPE. Nothing more is written to the child then,
|
|
1423
1444
|
// so the connection couldn't serve. It's read here, once the answer has
|
|
@@ -1430,8 +1451,8 @@ export class McplServerConnection extends EventEmitter {
|
|
|
1430
1451
|
// the launch if it can't reap the child. A failure of the write below
|
|
1431
1452
|
// is reported only once the caller has wired the transport, so it
|
|
1432
1453
|
// reaches the connection as a loss, like any later one.
|
|
1433
|
-
const
|
|
1434
|
-
if (
|
|
1454
|
+
const stopCause = transport.stdioStopCause;
|
|
1455
|
+
if (stopCause) throw failed(`MCPL server "${config.id}": this host stopped using the server's stdio during the handshake (${stopCause.message})`);
|
|
1435
1456
|
|
|
1436
1457
|
// Send `initialized` notification (no id)
|
|
1437
1458
|
transport.writeLine(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }));
|
|
@@ -2163,12 +2184,8 @@ export class McplServerConnection extends EventEmitter {
|
|
|
2163
2184
|
'not-sent',
|
|
2164
2185
|
));
|
|
2165
2186
|
}
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
// sent rather than left to wait out its timeout. The method and the
|
|
2169
|
-
// pipe's error are the operator's, so they go to the log.
|
|
2170
|
-
const pipeFailure = this.transport?.pipeFailure;
|
|
2171
|
-
if (pipeFailure) {
|
|
2187
|
+
const stopCause = this.transport?.stdioStopCause;
|
|
2188
|
+
if (stopCause) {
|
|
2172
2189
|
return McplServerConnection.refused(
|
|
2173
2190
|
new McplRequestError(
|
|
2174
2191
|
`The request wasn't sent: this host can no longer write to the "${this.id}" server, so nothing ran. ` +
|
|
@@ -2177,7 +2194,7 @@ export class McplServerConnection extends EventEmitter {
|
|
|
2177
2194
|
: "This host won't reconnect to it on its own. If you need it, ask whoever runs this host."),
|
|
2178
2195
|
'not-sent',
|
|
2179
2196
|
),
|
|
2180
|
-
`MCPL server "${this.id}": ${method} not sent, since
|
|
2197
|
+
`MCPL server "${this.id}": ${method} not sent, since this host stopped using the server's stdio (${logDetail(stopCause.message)})`,
|
|
2181
2198
|
);
|
|
2182
2199
|
}
|
|
2183
2200
|
|
|
@@ -2492,6 +2509,7 @@ export class McplServerConnection extends EventEmitter {
|
|
|
2492
2509
|
private responderFor(id: string | number, epoch: number) {
|
|
2493
2510
|
return {
|
|
2494
2511
|
id,
|
|
2512
|
+
signal: epoch === this.transportEpoch ? this.transportEnded.signal : AbortSignal.abort(),
|
|
2495
2513
|
respond: (result: unknown) => this.sendResponse(id, result, epoch),
|
|
2496
2514
|
respondError: (code: number, message: string, data?: unknown) =>
|
|
2497
2515
|
this.sendErrorResponse(id, code, message, data, epoch),
|
|
@@ -2787,6 +2805,7 @@ export class McplServerConnection extends EventEmitter {
|
|
|
2787
2805
|
signal: info.signal ?? null,
|
|
2788
2806
|
reason: info.reason ?? (byHost ? 'closed by host' : undefined),
|
|
2789
2807
|
...(info.reasonFromPeer ? { reasonFromPeer: true } : {}),
|
|
2808
|
+
...(info.pipeFailed ? { pipeFailed: true } : {}),
|
|
2790
2809
|
byHost,
|
|
2791
2810
|
};
|
|
2792
2811
|
this.emit('close', closeInfo.code, closeInfo.signal, closeInfo);
|
|
@@ -2798,20 +2817,10 @@ export class McplServerConnection extends EventEmitter {
|
|
|
2798
2817
|
}
|
|
2799
2818
|
};
|
|
2800
2819
|
transport.on('close', lost);
|
|
2801
|
-
// A stdio child whose stdin or stdout failed, and that outlived SIGKILL
|
|
2802
|
-
// too: the transport stays open, owning the child. The connection holds
|
|
2803
|
-
// it as a launch it can't reap, as a reconnect does, which halts
|
|
2804
|
-
// reconnecting, and is lost as on a close, with the permanent failure
|
|
2805
|
-
// that raises the alert and takes its tools off the list. Its id stays
|
|
2806
|
-
// held until close() confirms the exit. A close the host has begun
|
|
2807
|
-
// reports the failure itself. During a handshake nothing listens yet: a
|
|
2808
|
-
// handshake whose pipe has failed fails, when its answer comes (it's
|
|
2809
|
-
// refused then), when the child's exit closes the transport, or at its
|
|
2810
|
-
// timeout, and its own cleanup holds the launch.
|
|
2811
2820
|
transport.on('unreaped', (failure: Error) => {
|
|
2812
2821
|
if (this.closed || this.transport !== transport) return;
|
|
2813
2822
|
this.holdUnreaped(transport, failure);
|
|
2814
|
-
lost({ reason: `
|
|
2823
|
+
lost({ reason: `this host stopped using the server's stdio (${transport.stdioStopCause?.message ?? 'an error'}), and the server's process outlived SIGKILL` });
|
|
2815
2824
|
this.emit('reconnect-failed', { error: this.unreapedFailure(), attempt: Math.max(1, this.reconnectAttempts), permanent: true });
|
|
2816
2825
|
});
|
|
2817
2826
|
}
|
|
@@ -94,22 +94,25 @@ const schemaOf = (tool: HintedTool): JsonSchema => (isObj(tool.inputSchema) ? to
|
|
|
94
94
|
|
|
95
95
|
/** `play(point: string)`, `move(forward?: int -10..10 =0, turn?: int)` */
|
|
96
96
|
export function signature(tool: HintedTool): string {
|
|
97
|
-
|
|
98
|
-
const props = (isObj(s.properties) ? s.properties : {}) as Record<string, JsonSchema>;
|
|
99
|
-
const req = new Set(Array.isArray(s.required) ? (s.required as string[]) : []);
|
|
100
|
-
const params = Object.entries(props).map(([k, v]) => {
|
|
101
|
-
const def = isObj(v) && v.default !== undefined ? ` =${literal(v.default)}` : '';
|
|
102
|
-
return `${nameToTypeBack(k)}${req.has(k) ? '' : '?'}: ${typeSig(v, 1)}${def}`;
|
|
103
|
-
});
|
|
104
|
-
return `${nameToTypeBack(String(tool.name))}(${params.join(', ')})`;
|
|
97
|
+
return `${nameToTypeBack(String(tool.name))}(${paramsOf(schemaOf(tool))})`;
|
|
105
98
|
}
|
|
106
99
|
|
|
107
100
|
/** An options schema as `{size?: 9|13|19 =9, …}` (a toolset's options). */
|
|
108
101
|
export function objectSignature(schema: unknown): string {
|
|
109
|
-
const inner =
|
|
102
|
+
const inner = paramsOf(isObj(schema) ? schema : {});
|
|
110
103
|
return inner ? `{${inner}}` : '{}';
|
|
111
104
|
}
|
|
112
105
|
|
|
106
|
+
/** A schema's properties as `name?: type =default`, joined with commas. */
|
|
107
|
+
function paramsOf(s: JsonSchema): string {
|
|
108
|
+
const props = (isObj(s.properties) ? s.properties : {}) as Record<string, JsonSchema>;
|
|
109
|
+
const req = new Set(Array.isArray(s.required) ? (s.required as string[]) : []);
|
|
110
|
+
return Object.entries(props).map(([k, v]) => {
|
|
111
|
+
const def = isObj(v) && v.default !== undefined ? ` =${literal(v.default)}` : '';
|
|
112
|
+
return `${nameToTypeBack(k)}${req.has(k) ? '' : '?'}: ${typeSig(v, 1)}${def}`;
|
|
113
|
+
}).join(', ');
|
|
114
|
+
}
|
|
115
|
+
|
|
113
116
|
/** First sentence of a description, bounded. */
|
|
114
117
|
export function firstSentence(text: unknown, max = 140): string {
|
|
115
118
|
const t = (typeof text === 'string' ? text : '').trim().replace(/\s+/g, ' ');
|
|
@@ -153,11 +156,15 @@ export function renderToolLines(tools: HintedTool[], shown: Shown, opts: { descr
|
|
|
153
156
|
const d = toolDigest(t);
|
|
154
157
|
const describe = opts.describeAll || !shown.has(d);
|
|
155
158
|
shown.add(d);
|
|
156
|
-
|
|
157
|
-
return `${indent}${signature(t)}${sentence ? ` — ${suppliedValue(sentence)}` : ''}`;
|
|
159
|
+
return toolLine(t, describe, indent);
|
|
158
160
|
});
|
|
159
161
|
}
|
|
160
162
|
|
|
163
|
+
export function toolLine(tool: HintedTool, describe = true, indent = ' '): string {
|
|
164
|
+
const sentence = describe ? firstSentence(tool.description) : '';
|
|
165
|
+
return `${indent}${signature(tool)}${sentence ? ` — ${suppliedValue(sentence)}` : ''}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
161
168
|
/** A server's value as JSON, or a note where it's nested too deeply, or too long, to show. */
|
|
162
169
|
export function jsonText(value: unknown, space?: number): string {
|
|
163
170
|
if (!nestsWithin(value)) return '(nested too deeply to show)';
|