@animalabs/agent-framework 0.28.0 → 0.30.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 +88 -25
- package/dist/src/admission/admit.d.ts +90 -0
- package/dist/src/admission/admit.d.ts.map +1 -0
- package/dist/src/admission/admit.js +43 -0
- package/dist/src/admission/admit.js.map +1 -0
- package/dist/src/agent.d.ts +21 -0
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +47 -7
- 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 +3 -12
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +7 -3
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/author-presentation.d.ts +50 -0
- package/dist/src/author-presentation.d.ts.map +1 -0
- package/dist/src/author-presentation.js +58 -0
- package/dist/src/author-presentation.js.map +1 -0
- package/dist/src/auto-continue.d.ts +2 -0
- package/dist/src/auto-continue.d.ts.map +1 -1
- package/dist/src/auto-continue.js +5 -2
- package/dist/src/auto-continue.js.map +1 -1
- package/dist/src/code-execution/isolate/runner.d.ts +7 -5
- package/dist/src/code-execution/isolate/runner.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/runner.js +7 -5
- package/dist/src/code-execution/isolate/runner.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +3 -2
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/dendrite/index.d.ts +6 -0
- package/dist/src/dendrite/index.d.ts.map +1 -0
- package/dist/src/dendrite/index.js +3 -0
- package/dist/src/dendrite/index.js.map +1 -0
- package/dist/src/dendrite/presets.d.ts +60 -0
- package/dist/src/dendrite/presets.d.ts.map +1 -0
- package/dist/src/dendrite/presets.js +117 -0
- package/dist/src/dendrite/presets.js.map +1 -0
- package/dist/src/dendrite/registry.d.ts +260 -0
- package/dist/src/dendrite/registry.d.ts.map +1 -0
- package/dist/src/dendrite/registry.js +742 -0
- package/dist/src/dendrite/registry.js.map +1 -0
- package/dist/src/dendrite/types.d.ts +392 -0
- package/dist/src/dendrite/types.d.ts.map +1 -0
- package/dist/src/dendrite/types.js +24 -0
- package/dist/src/dendrite/types.js.map +1 -0
- package/dist/src/framework.d.ts +846 -67
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +4213 -924
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +46 -14
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +143 -53
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/index.d.ts +1 -1
- 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/types.d.ts +12 -2
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/heartbeat/heartbeat.d.ts +18 -0
- package/dist/src/heartbeat/heartbeat.d.ts.map +1 -1
- package/dist/src/heartbeat/heartbeat.js +34 -8
- package/dist/src/heartbeat/heartbeat.js.map +1 -1
- package/dist/src/held-writes.d.ts +3 -3
- package/dist/src/held-writes.js +3 -3
- package/dist/src/host-presentation.d.ts +174 -0
- package/dist/src/host-presentation.d.ts.map +1 -0
- package/dist/src/host-presentation.js +565 -0
- package/dist/src/host-presentation.js.map +1 -0
- package/dist/src/index.d.ts +7 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/line-reader.d.ts +4 -0
- package/dist/src/line-reader.d.ts.map +1 -0
- package/dist/src/line-reader.js +59 -0
- package/dist/src/line-reader.js.map +1 -0
- package/dist/src/mcpl/channel-registry.d.ts +118 -15
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +266 -110
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +2 -1
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts +1 -0
- package/dist/src/mcpl/convert-block.d.ts.map +1 -1
- package/dist/src/mcpl/convert-block.js +16 -4
- package/dist/src/mcpl/convert-block.js.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.js +4 -3
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +3 -1
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
- package/dist/src/mcpl/inbound-source.js +3 -1
- package/dist/src/mcpl/inbound-source.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +11 -7
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +3 -2
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +25 -3
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +9 -0
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +21 -0
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/push-handler.js +2 -2
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +12 -40
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +29 -23
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +20 -20
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/skip-reply-wake.d.ts +25 -0
- package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -1
- package/dist/src/mcpl/skip-reply-wake.js +25 -0
- package/dist/src/mcpl/skip-reply-wake.js.map +1 -1
- package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
- package/dist/src/mcpl/tool-presence.js +18 -17
- package/dist/src/mcpl/tool-presence.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
- package/dist/src/mcpl/tool-signature.js +17 -16
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/toolset-host.d.ts +8 -26
- package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
- package/dist/src/mcpl/toolset-host.js +16 -6
- package/dist/src/mcpl/toolset-host.js.map +1 -1
- package/dist/src/mcpl/toolsets-module.d.ts +14 -25
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
- package/dist/src/mcpl/toolsets-module.js +187 -50
- package/dist/src/mcpl/toolsets-module.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +1 -1
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +16 -16
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +28 -12
- 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 +6 -7
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +6 -7
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/api/index.d.ts.map +1 -1
- package/dist/src/modules/api/index.js +4 -2
- package/dist/src/modules/api/index.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +33 -25
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/history/index.js +8 -6
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/sender-name.d.ts +7 -0
- package/dist/src/modules/sender-name.d.ts.map +1 -0
- package/dist/src/modules/sender-name.js +10 -0
- package/dist/src/modules/sender-name.js.map +1 -0
- package/dist/src/modules/workspace/answers.d.ts +5 -1
- package/dist/src/modules/workspace/answers.d.ts.map +1 -1
- package/dist/src/modules/workspace/answers.js +16 -1
- package/dist/src/modules/workspace/answers.js.map +1 -1
- package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
- package/dist/src/modules/workspace/direct-write.js +6 -4
- package/dist/src/modules/workspace/direct-write.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts +4 -2
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +50 -40
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +1 -0
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +66 -36
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +51 -20
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +120 -82
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +7 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +133 -37
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/notice-tags.d.ts +81 -0
- package/dist/src/notice-tags.d.ts.map +1 -0
- package/dist/src/notice-tags.js +85 -0
- package/dist/src/notice-tags.js.map +1 -0
- package/dist/src/reasoning-loss-notice.d.ts.map +1 -1
- package/dist/src/reasoning-loss-notice.js +5 -3
- package/dist/src/reasoning-loss-notice.js.map +1 -1
- package/dist/src/silent-heartbeat.d.ts +5 -55
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js +63 -7
- package/dist/src/silent-heartbeat.js.map +1 -1
- package/dist/src/stored-injections.d.ts +4 -0
- package/dist/src/stored-injections.d.ts.map +1 -1
- package/dist/src/stored-injections.js +6 -0
- package/dist/src/stored-injections.js.map +1 -1
- package/dist/src/supplied-text.d.ts +35 -0
- package/dist/src/supplied-text.d.ts.map +1 -0
- package/dist/src/supplied-text.js +141 -0
- package/dist/src/supplied-text.js.map +1 -0
- package/dist/src/tool-args.d.ts.map +1 -1
- package/dist/src/tool-args.js +9 -8
- package/dist/src/tool-args.js.map +1 -1
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js +2 -1
- 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 +5 -4
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +382 -11
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +1100 -119
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +101 -5
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/tune-out/tools.js +55 -18
- package/dist/src/tune-out/tools.js.map +1 -1
- package/dist/src/types/agent.d.ts +48 -9
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +142 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +22 -11
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +136 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/admission/admit.ts +113 -0
- package/src/agent.ts +52 -7
- package/src/api/mcp-server-core.ts +3 -14
- package/src/api/server.ts +7 -3
- package/src/author-presentation.ts +81 -0
- package/src/auto-continue.ts +8 -2
- package/src/code-execution/isolate/runner.ts +7 -5
- package/src/code-execution/isolate/scripts.ts +3 -2
- package/src/dendrite/index.ts +32 -0
- package/src/dendrite/presets.ts +149 -0
- package/src/dendrite/registry.ts +808 -0
- package/src/dendrite/types.ts +397 -0
- package/src/framework.ts +4362 -852
- package/src/gate/event-gate.ts +159 -52
- package/src/gate/index.ts +1 -1
- package/src/gate/types.ts +12 -2
- package/src/heartbeat/heartbeat.ts +48 -9
- package/src/held-writes.ts +3 -3
- package/src/host-presentation.ts +612 -0
- package/src/index.ts +17 -4
- package/src/line-reader.ts +58 -0
- package/src/mcpl/channel-registry.ts +306 -111
- package/src/mcpl/conversation-router.ts +2 -1
- package/src/mcpl/convert-block.ts +17 -4
- package/src/mcpl/hook-orchestrator.ts +4 -3
- package/src/mcpl/inbound-source.ts +3 -1
- package/src/mcpl/modern-connection.ts +11 -7
- package/src/mcpl/protocol-family.ts +24 -3
- package/src/mcpl/push-coalescer.ts +19 -0
- package/src/mcpl/push-handler.ts +2 -2
- package/src/mcpl/references.ts +31 -23
- package/src/mcpl/server-connection.ts +20 -18
- package/src/mcpl/skip-reply-wake.ts +35 -0
- package/src/mcpl/tool-presence.ts +18 -17
- package/src/mcpl/tool-signature.ts +17 -16
- package/src/mcpl/toolset-host.ts +18 -6
- package/src/mcpl/toolsets-module.ts +149 -50
- package/src/mcpl/transport.ts +18 -14
- package/src/mcpl/types.ts +29 -12
- package/src/module-registry.ts +6 -7
- package/src/modules/api/index.ts +4 -2
- package/src/modules/discord/index.ts +37 -25
- package/src/modules/history/index.ts +8 -6
- package/src/modules/sender-name.ts +9 -0
- package/src/modules/workspace/answers.ts +20 -2
- package/src/modules/workspace/direct-write.ts +4 -3
- package/src/modules/workspace/effects.ts +50 -40
- package/src/modules/workspace/index.ts +58 -34
- package/src/modules/workspace/observe.ts +139 -88
- package/src/modules/workspace/reconcile.ts +159 -36
- package/src/notice-tags.ts +86 -0
- package/src/reasoning-loss-notice.ts +5 -3
- package/src/silent-heartbeat.ts +11 -7
- package/src/stored-injections.ts +7 -0
- package/src/supplied-text.ts +154 -0
- package/src/tool-args.ts +9 -8
- package/src/tool-image-ledger.ts +2 -1
- package/src/tool-presentation.ts +5 -4
- package/src/tune-out/coordinator.ts +1252 -123
- package/src/tune-out/tools.ts +152 -11
- package/src/types/agent.ts +51 -9
- package/src/types/framework.ts +146 -0
- package/src/types/module.ts +23 -11
- package/src/types/trace.ts +150 -0
package/src/mcpl/transport.ts
CHANGED
|
@@ -19,9 +19,10 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { spawn, type ChildProcess } from 'node:child_process';
|
|
22
|
-
import { createInterface, type Interface as ReadlineInterface } from 'node:readline';
|
|
23
22
|
import { EventEmitter } from 'node:events';
|
|
23
|
+
import type { Readable } from 'node:stream';
|
|
24
24
|
import { StringDecoder } from 'node:string_decoder';
|
|
25
|
+
import { escapeLineSeparators, readLines } from '../line-reader.js';
|
|
25
26
|
import WebSocket from 'ws';
|
|
26
27
|
|
|
27
28
|
import type { McplServerConfig } from './types.js';
|
|
@@ -325,16 +326,18 @@ export class StdioTransport extends McplTransport {
|
|
|
325
326
|
});
|
|
326
327
|
}
|
|
327
328
|
|
|
329
|
+
private readonly stopLines: () => void;
|
|
330
|
+
|
|
328
331
|
private constructor(
|
|
329
332
|
private readonly child: ChildProcess,
|
|
330
|
-
|
|
333
|
+
stdout: Readable,
|
|
331
334
|
/** The server this child serves, named in its stderr lines and in what
|
|
332
335
|
* its reaping reports. */
|
|
333
336
|
serverId: string,
|
|
334
337
|
) {
|
|
335
338
|
super(serverId);
|
|
336
339
|
|
|
337
|
-
this.
|
|
340
|
+
this.stopLines = readLines(stdout, (line) => this.pushLine(line));
|
|
338
341
|
|
|
339
342
|
// Stitch stderr chunks into whole lines (a chunk can split a line, and a
|
|
340
343
|
// character's bytes), and keep the stream's last characters for
|
|
@@ -383,12 +386,7 @@ export class StdioTransport extends McplTransport {
|
|
|
383
386
|
});
|
|
384
387
|
};
|
|
385
388
|
this.child.stdin?.on('error', endOnPipeError);
|
|
386
|
-
// On the stream itself, since close() closes readline, which takes its own
|
|
387
|
-
// listener off the stream. While readline is open, its listener, which
|
|
388
|
-
// came first, re-emits the error on the interface, where an empty
|
|
389
|
-
// listener absorbs that copy.
|
|
390
389
|
this.child.stdout?.on('error', endOnPipeError);
|
|
391
|
-
this.rl.on('error', () => { /* readline's copy of a stdout error, handled above */ });
|
|
392
390
|
// stderr carries diagnostics only: a read error on it is reported, to the
|
|
393
391
|
// listeners or else in the log, and never thrown. It ends nothing once
|
|
394
392
|
// the connection is wired; during a handshake the listener is the
|
|
@@ -453,8 +451,7 @@ export class StdioTransport extends McplTransport {
|
|
|
453
451
|
resolve();
|
|
454
452
|
});
|
|
455
453
|
});
|
|
456
|
-
|
|
457
|
-
return new StdioTransport(child, rl, config.id);
|
|
454
|
+
return new StdioTransport(child, child.stdout!, config.id);
|
|
458
455
|
}
|
|
459
456
|
|
|
460
457
|
writeLine(json: string): void {
|
|
@@ -462,7 +459,7 @@ export class StdioTransport extends McplTransport {
|
|
|
462
459
|
// transport is ending it.
|
|
463
460
|
if (this.pipeError) return;
|
|
464
461
|
// stdin may be gone if the child died between the closed-check and here.
|
|
465
|
-
this.child.stdin?.write(json + '\n');
|
|
462
|
+
this.child.stdin?.write(escapeLineSeparators(json) + '\n');
|
|
466
463
|
}
|
|
467
464
|
|
|
468
465
|
private closing: Promise<void> | null = null;
|
|
@@ -492,7 +489,7 @@ export class StdioTransport extends McplTransport {
|
|
|
492
489
|
|
|
493
490
|
private async reap(): Promise<void> {
|
|
494
491
|
this.markClosed({ reason: 'closed by host' });
|
|
495
|
-
this.
|
|
492
|
+
this.stopLines();
|
|
496
493
|
await this.terminate();
|
|
497
494
|
}
|
|
498
495
|
|
|
@@ -696,7 +693,7 @@ export class WebSocketTransport extends McplTransport {
|
|
|
696
693
|
|
|
697
694
|
writeLine(json: string): void {
|
|
698
695
|
if (this.ws.readyState === WebSocket.OPEN) {
|
|
699
|
-
this.ws.send(json);
|
|
696
|
+
this.ws.send(escapeLineSeparators(json));
|
|
700
697
|
}
|
|
701
698
|
}
|
|
702
699
|
|
|
@@ -830,7 +827,7 @@ const SERVER_CONFIG_BOOLEANS: Record<BooleanSetting, string> = {
|
|
|
830
827
|
inheritEnv: 'true passes the server the host\'s whole environment, and false or leaving it out passes only the allowlisted and LC_* host variables',
|
|
831
828
|
reconnect: 'true retries in the background when the server can\'t be reached or its connection drops, and false or leaving it out doesn\'t',
|
|
832
829
|
allowHostCommands: 'true lets the server run the host\'s admin commands, such as undo, hide, unstick and quiesce, and false or leaving it out refuses them',
|
|
833
|
-
toolHints: 'true advertises MCPL tool hints (RFC-014), so
|
|
830
|
+
toolHints: 'true advertises MCPL tool hints (RFC-014) to an MCPL server, so its dynamic tools are announced in the conversation and called through toolsets--act; leaving it out does the same wherever the name toolsets is free; false lists every tool as before and advertises none',
|
|
834
831
|
};
|
|
835
832
|
|
|
836
833
|
/**
|
|
@@ -849,6 +846,13 @@ export function assertServerConfigBooleans(config: McplServerConfig): void {
|
|
|
849
846
|
if (setting === 'inheritEnv' && dialsNetwork(config)) continue;
|
|
850
847
|
assertBooleanSetting(`MCPL server "${config.id}" ${setting}`, config[setting], whatTrueDoes);
|
|
851
848
|
}
|
|
849
|
+
// Not a boolean, but read the same way: a value that isn't one of the
|
|
850
|
+
// three would otherwise be taken for the default.
|
|
851
|
+
const presentation = (config as { presentation?: unknown }).presentation;
|
|
852
|
+
if (presentation !== undefined && presentation !== null && presentation !== 'full' && presentation !== 'tab' && presentation !== 'closed-tab') {
|
|
853
|
+
const kind = typeof presentation === 'string' ? 'another string' : Array.isArray(presentation) ? 'a list' : typeof presentation;
|
|
854
|
+
throw new Error(`MCPL server "${config.id}" presentation must be "full", "tab" or "closed-tab" (or left out for full), not ${kind}`);
|
|
855
|
+
}
|
|
852
856
|
}
|
|
853
857
|
|
|
854
858
|
/**
|
package/src/mcpl/types.ts
CHANGED
|
@@ -193,8 +193,8 @@ export interface McplHostCapabilities {
|
|
|
193
193
|
modelInfo?: boolean;
|
|
194
194
|
featureSets?: boolean;
|
|
195
195
|
channels?: boolean | McplChannelCapabilities;
|
|
196
|
-
/** RFC-014 §6.3: this host presents tool hints. Sent
|
|
197
|
-
*
|
|
196
|
+
/** RFC-014 §6.3: this host presents tool hints. Sent to every server
|
|
197
|
+
* with hints (McplServerConfig.toolHints: on unless false). */
|
|
198
198
|
toolHints?: boolean;
|
|
199
199
|
}
|
|
200
200
|
|
|
@@ -508,18 +508,35 @@ export interface McplServerConfig {
|
|
|
508
508
|
toolLifecycle?: import('./tool-lifecycle.js').ToolLifecycleConfig;
|
|
509
509
|
|
|
510
510
|
/**
|
|
511
|
-
* MCPL RFC-014 (tool presence, toolsets and surfaces).
|
|
512
|
-
* host advertises
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
* `toolsets--
|
|
520
|
-
*
|
|
511
|
+
* MCPL RFC-014 (tool presence, toolsets and surfaces). On unless set to
|
|
512
|
+
* `false`: the host advertises `experimental.mcpl.toolHints` and honours
|
|
513
|
+
* hints (through the `toolsets` meta-tools) only from a server that
|
|
514
|
+
* advertised `toolHints` too. Such a server's static tools stay in the tool
|
|
515
|
+
* list as before, and its dynamic tools (toolset tools, `mcpl/presence:
|
|
516
|
+
* "dynamic"`) leave it: they are announced in the conversation as they
|
|
517
|
+
* become visible and invisible, and called through `toolsets--act`, opened
|
|
518
|
+
* with `toolsets--open`, inspected with `toolsets--look` and closed with
|
|
519
|
+
* `toolsets--close`. A server that doesn't advertise `toolHints` lists every
|
|
520
|
+
* tool as before. `false` advertises nothing and treats every tool as
|
|
521
|
+
* static. Omitted and null are on unless a module given to `create()`
|
|
522
|
+
* holds the name `toolsets` (or a restart is moving a server to that
|
|
523
|
+
* prefix, which is refused); `true` with such a module is refused.
|
|
521
524
|
*/
|
|
522
525
|
toolHints?: boolean;
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* How this server is presented to agents at first (the host's own
|
|
529
|
+
* toolsets, src/host-presentation.ts): `'full'` (the default) puts its
|
|
530
|
+
* tools in the tool list, as before; `'tab'` keeps it connected, its events
|
|
531
|
+
* still arriving, with none of its tools in the tool list until an agent
|
|
532
|
+
* opens its tab (`toolsets--open {server}`) and calls them through
|
|
533
|
+
* `toolsets--act`; `'closed-tab'` leaves it disconnected until an agent
|
|
534
|
+
* opens it. Each agent can switch it later (`toolsets--close {server}`,
|
|
535
|
+
* with `disconnect: true` for a closed tab; `toolsets--open {server,
|
|
536
|
+
* full: true}` back into the tool list), and that choice is kept across
|
|
537
|
+
* restarts beside the store, over this default.
|
|
538
|
+
*/
|
|
539
|
+
presentation?: 'full' | 'tab' | 'closed-tab';
|
|
523
540
|
}
|
|
524
541
|
|
|
525
542
|
// ============================================================================
|
package/src/module-registry.ts
CHANGED
|
@@ -282,8 +282,8 @@ export class ModuleRegistry {
|
|
|
282
282
|
/**
|
|
283
283
|
* All utilities from all modules, namespaced exactly like tools
|
|
284
284
|
* (`module--name`). See Module.getUtilities — these are deliberately kept
|
|
285
|
-
* out of the model-facing tool list and
|
|
286
|
-
*
|
|
285
|
+
* out of the model-facing tool list and presented in the host's built-in
|
|
286
|
+
* toolset; dispatch goes through the same handleToolCall.
|
|
287
287
|
*/
|
|
288
288
|
getAllUtilities(): ToolDefinition[] {
|
|
289
289
|
const utilities: ToolDefinition[] = [];
|
|
@@ -300,8 +300,8 @@ export class ModuleRegistry {
|
|
|
300
300
|
|
|
301
301
|
/**
|
|
302
302
|
* A module tool's call for a path that stores or sends its result: the
|
|
303
|
-
* model's dispatch
|
|
304
|
-
*
|
|
303
|
+
* model's dispatch (a `toolsets--act` call of it included), a script's
|
|
304
|
+
* inner call. The result is checked and read once, in its JSON form
|
|
305
305
|
* ({@link storableToolResult}).
|
|
306
306
|
*/
|
|
307
307
|
async storableToolCall(call: ToolCall, options: { definition?: ToolDefinition } = {}): Promise<ToolResult> {
|
|
@@ -320,9 +320,8 @@ export class ModuleRegistry {
|
|
|
320
320
|
* So is a call membrane marked `unparseableInput`, unless its text is JSON
|
|
321
321
|
* `null`, which reaches the module as a call with no arguments
|
|
322
322
|
* (markedInputRefusal).
|
|
323
|
-
* A caller that has already resolved the tool's definition
|
|
324
|
-
*
|
|
325
|
-
* that one.
|
|
323
|
+
* A caller that has already resolved the tool's definition passes it as
|
|
324
|
+
* `definition`, and it's checked against that one.
|
|
326
325
|
*/
|
|
327
326
|
async handleToolCall(given: ToolCall, options: { definition?: ToolDefinition } = {}): Promise<ToolResult> {
|
|
328
327
|
const marked = markedInputRefusal(given);
|
package/src/modules/api/index.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { unknownToolMessage } from '../unknown-tool.js';
|
|
9
|
+
import { outsideSenderName } from '../sender-name.js';
|
|
9
10
|
import type {
|
|
10
11
|
Module,
|
|
11
12
|
ModuleContext,
|
|
@@ -62,12 +63,13 @@ export class ApiModule implements Module {
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
private handleMessage(event: ApiMessageEvent): EventResponse {
|
|
66
|
+
const carried = event.metadata?.author;
|
|
65
67
|
const response: EventResponse = {
|
|
66
68
|
addMessages: [
|
|
67
69
|
{
|
|
68
|
-
participant: event.participant,
|
|
70
|
+
participant: outsideSenderName(event.participant, 'API'),
|
|
69
71
|
content: [{ type: 'text', text: event.content }],
|
|
70
|
-
metadata: event.metadata,
|
|
72
|
+
metadata: { ...event.metadata, author: typeof carried === 'object' && carried !== null ? carried : { name: event.participant } },
|
|
71
73
|
},
|
|
72
74
|
],
|
|
73
75
|
};
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* - Tools for explicit Discord operations (send, DM, react, etc.)
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { NOTICE_TAG } from '../../notice-tags.js';
|
|
11
|
+
import { markedText, suppliedValue } from '../../supplied-text.js';
|
|
10
12
|
import type { ContentBlock } from '@animalabs/membrane';
|
|
11
13
|
import { continuesBody, type StoredMessage } from '@animalabs/context-manager';
|
|
12
14
|
import type {
|
|
@@ -47,6 +49,7 @@ import type {
|
|
|
47
49
|
import { formatZonedDateTime, resolveTimeZone } from '../../timezone.js';
|
|
48
50
|
import { logValue } from '../../mcpl/log-value.js';
|
|
49
51
|
import { unknownToolMessage } from '../unknown-tool.js';
|
|
52
|
+
import { outsideSenderName } from '../sender-name.js';
|
|
50
53
|
import { DiscordRefusal } from './refusal.js';
|
|
51
54
|
|
|
52
55
|
// Re-export event types for consumers who want to handle them
|
|
@@ -119,10 +122,11 @@ const NOT_RUNNING = "Speech handling wasn't changed: the Discord module hasn't s
|
|
|
119
122
|
/** What the agent reads for a text attachment whose contents couldn't be
|
|
120
123
|
* fetched: by an error, or by an answer that wasn't OK. */
|
|
121
124
|
function attachmentNotFetched(filename: string): { type: 'text'; text: string } {
|
|
122
|
-
return {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
return { type: 'text', text: notFetchedText(suppliedValue(filename)) };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function notFetchedText(filename: string): string {
|
|
129
|
+
return `[${NOTICE_TAG.attachment}: ${filename} couldn't be fetched, so its text isn't in your context. If you need it, ask the sender in the channel to paste it.]`;
|
|
126
130
|
}
|
|
127
131
|
|
|
128
132
|
/**
|
|
@@ -165,7 +169,7 @@ function seconds(n: number): string {
|
|
|
165
169
|
|
|
166
170
|
/** What a stored message reads when it has no text of its own and nothing the
|
|
167
171
|
* host added, so the agent still sees that a message was there. */
|
|
168
|
-
const EMPTY_MESSAGE =
|
|
172
|
+
const EMPTY_MESSAGE = `[${NOTICE_TAG.emptyMessage}]`;
|
|
169
173
|
|
|
170
174
|
/**
|
|
171
175
|
* Metadata on a stored Discord message whose first block is its own text,
|
|
@@ -184,6 +188,14 @@ function ownText(content: string): string {
|
|
|
184
188
|
return content.trim() ? content : '';
|
|
185
189
|
}
|
|
186
190
|
|
|
191
|
+
function storedOwnText(content: string): string {
|
|
192
|
+
return markedText(ownText(content));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function holdsOwnText(text: string, own: string): boolean {
|
|
196
|
+
return text === own || text === markedText(own);
|
|
197
|
+
}
|
|
198
|
+
|
|
187
199
|
const isEmptyMarker = (block: ContentBlock): boolean => block.type === 'text' && block.text === EMPTY_MESSAGE;
|
|
188
200
|
const isBlankText = (block: ContentBlock): boolean => block.type === 'text' && !block.text.trim();
|
|
189
201
|
|
|
@@ -198,7 +210,7 @@ function textFirst(text: string, after: ContentBlock[] = []): ContentBlock[] {
|
|
|
198
210
|
/** A text-first row's content with `text` as the message's own text, in
|
|
199
211
|
* place of its first block, keeping what the host added after it. */
|
|
200
212
|
function withOwnText(content: ContentBlock[], text: string): ContentBlock[] {
|
|
201
|
-
return textFirst(
|
|
213
|
+
return textFirst(storedOwnText(text), content.slice(1).filter((block) => !isEmptyMarker(block)));
|
|
202
214
|
}
|
|
203
215
|
|
|
204
216
|
/**
|
|
@@ -221,11 +233,11 @@ function contentForText(stored: StoredMessage, discordText: string): ContentBloc
|
|
|
221
233
|
const text = ownText(discordText);
|
|
222
234
|
const [first, ...rest] = stored.content;
|
|
223
235
|
if (stored.metadata?.[TEXT_FIRST] === true) {
|
|
224
|
-
if (first?.type === 'text' && first.text
|
|
236
|
+
if (first?.type === 'text' && holdsOwnText(first.text, text)) return null;
|
|
225
237
|
return withOwnText(stored.content, text);
|
|
226
238
|
}
|
|
227
|
-
if (!text || (first?.type === 'text' && first.text
|
|
228
|
-
return [{ type: 'text', text: discordText }, ...(first?.type === 'text' ? rest : stored.content).filter((block) => !isEmptyMarker(block))];
|
|
239
|
+
if (!text || (first?.type === 'text' && holdsOwnText(first.text, discordText))) return null;
|
|
240
|
+
return [{ type: 'text', text: markedText(discordText) }, ...(first?.type === 'text' ? rest : stored.content).filter((block) => !isEmptyMarker(block))];
|
|
229
241
|
}
|
|
230
242
|
|
|
231
243
|
/**
|
|
@@ -255,7 +267,7 @@ function needsRebuild(stored: StoredMessage, message: HistoryMessage): boolean {
|
|
|
255
267
|
const [first, ...rest] = stored.content;
|
|
256
268
|
if (!own && first?.type === 'text' && !isBlankText(first) && !isEmptyMarker(first)) return true;
|
|
257
269
|
const expected = (own ? 1 : 0) + attachments;
|
|
258
|
-
const counted = own && first?.type === 'text' && first.text
|
|
270
|
+
const counted = own && first?.type === 'text' && holdsOwnText(first.text, own) ? 1 + held(rest) : held(stored.content);
|
|
259
271
|
return counted !== expected || (expected === 0 && !stored.content.some(isEmptyMarker));
|
|
260
272
|
}
|
|
261
273
|
|
|
@@ -268,14 +280,14 @@ function needsRebuild(stored: StoredMessage, message: HistoryMessage): boolean {
|
|
|
268
280
|
function holdsOtherText(stored: StoredMessage, message: HistoryMessage): boolean {
|
|
269
281
|
const own = ownText(message.content);
|
|
270
282
|
const [first, ...rest] = stored.content;
|
|
271
|
-
const others = own && first?.type === 'text' && first.text
|
|
283
|
+
const others = own && first?.type === 'text' && holdsOwnText(first.text, own) ? rest : stored.content;
|
|
272
284
|
return others.some((block) => block.type === 'text' && !isBlankText(block) && !isEmptyMarker(block));
|
|
273
285
|
}
|
|
274
286
|
|
|
275
287
|
/** Whether a message's conversion holds, after the message's own text, the
|
|
276
288
|
* note for an attachment of its whose text wasn't fetched. */
|
|
277
289
|
function holdsNotFetched(content: ContentBlock[], message: HistoryMessage): boolean {
|
|
278
|
-
const notes = new Set((message.attachments ?? []).
|
|
290
|
+
const notes = new Set((message.attachments ?? []).flatMap((attachment) => [attachmentNotFetched(attachment.filename).text, notFetchedText(attachment.filename)]));
|
|
279
291
|
return content.slice(1).some((block) => block.type === 'text' && notes.has(block.text));
|
|
280
292
|
}
|
|
281
293
|
|
|
@@ -302,9 +314,10 @@ function forGoneAttachment(block: ContentBlock, attachments: readonly DiscordAtt
|
|
|
302
314
|
if (block.type !== 'text') return false;
|
|
303
315
|
const { text } = block;
|
|
304
316
|
const opening = text.startsWith('\n--- File: ') ? (filename: string) => `\n--- File: ${filename} ---\n`
|
|
305
|
-
: text.startsWith(
|
|
317
|
+
: text.startsWith(`[${NOTICE_TAG.attachment}: `) ? (filename: string) => `[${NOTICE_TAG.attachment}: ${filename} `
|
|
306
318
|
: null;
|
|
307
|
-
return opening !== null && !attachments.some((attachment) =>
|
|
319
|
+
return opening !== null && !attachments.some((attachment) =>
|
|
320
|
+
text.startsWith(opening(suppliedValue(attachment.filename))) || text.startsWith(opening(attachment.filename)));
|
|
308
321
|
}
|
|
309
322
|
|
|
310
323
|
/**
|
|
@@ -818,7 +831,7 @@ export class DiscordModule implements Module {
|
|
|
818
831
|
|
|
819
832
|
// Built from the message and its attachments, or from its text alone
|
|
820
833
|
// when an event carries no blocks; text first, as TEXT_FIRST marks.
|
|
821
|
-
const contentBlocks = msg.contentBlocks ?? textFirst(
|
|
834
|
+
const contentBlocks = msg.contentBlocks ?? textFirst(storedOwnText(msg.content));
|
|
822
835
|
this.noteLive(msg.discordMessageId);
|
|
823
836
|
|
|
824
837
|
// A message a history sync wrote first, stored or held, keeps that row:
|
|
@@ -833,9 +846,10 @@ export class DiscordModule implements Module {
|
|
|
833
846
|
...(stored ? {} : {
|
|
834
847
|
addMessages: [
|
|
835
848
|
{
|
|
836
|
-
participant: msg.authorName,
|
|
849
|
+
participant: outsideSenderName(msg.authorName, 'Discord'),
|
|
837
850
|
content: contentBlocks,
|
|
838
851
|
metadata: {
|
|
852
|
+
author: { id: msg.authorId, name: msg.authorName },
|
|
839
853
|
external: { source: 'discord', id: msg.discordMessageId },
|
|
840
854
|
channelId: msg.channelId,
|
|
841
855
|
timestamp: msg.timestamp,
|
|
@@ -1144,14 +1158,12 @@ export class DiscordModule implements Module {
|
|
|
1144
1158
|
// For bot's own messages (from previous sessions), use the agent name
|
|
1145
1159
|
// This ensures continuity of conversation history
|
|
1146
1160
|
const isBotMessage = discordMsg.authorId === this.state.botUserId;
|
|
1147
|
-
const agentName = ctx.getAgents()[0]?.name;
|
|
1148
|
-
const participantName = isBotMessage
|
|
1149
|
-
? (agentName ?? discordMsg.authorName)
|
|
1150
|
-
: discordMsg.authorName;
|
|
1161
|
+
const agentName = isBotMessage ? ctx.getAgents()[0]?.name : undefined;
|
|
1151
1162
|
ctx.addMessage(
|
|
1152
|
-
|
|
1163
|
+
agentName ?? outsideSenderName(discordMsg.authorName, 'Discord'),
|
|
1153
1164
|
content,
|
|
1154
1165
|
{
|
|
1166
|
+
...(agentName === undefined ? { author: { id: discordMsg.authorId, name: discordMsg.authorName } } : {}),
|
|
1155
1167
|
external: { source: 'discord', id: discordMsg.id },
|
|
1156
1168
|
channelId,
|
|
1157
1169
|
timestamp: discordMsg.timestamp.getTime(),
|
|
@@ -1371,7 +1383,7 @@ export class DiscordModule implements Module {
|
|
|
1371
1383
|
if (isTextFile && attachment.size >= TEXT_FILE_LIMIT_BYTES) {
|
|
1372
1384
|
blocks.push({
|
|
1373
1385
|
type: 'text',
|
|
1374
|
-
text: `[
|
|
1386
|
+
text: `[${NOTICE_TAG.attachment}: ${suppliedValue(attachment.filename)} (${attachment.size.toLocaleString('en-US')} bytes): this host reads text files under ${TEXT_FILE_LIMIT_BYTES.toLocaleString('en-US')} bytes into your context, so its text isn't here. If you need it, ask the sender to paste the part that matters.]`,
|
|
1375
1387
|
});
|
|
1376
1388
|
continue;
|
|
1377
1389
|
}
|
|
@@ -1383,7 +1395,7 @@ export class DiscordModule implements Module {
|
|
|
1383
1395
|
const textContent = await response.text();
|
|
1384
1396
|
blocks.push({
|
|
1385
1397
|
type: 'text',
|
|
1386
|
-
text: `\n--- File: ${attachment.filename} ---\n${textContent}\n--- End of ${attachment.filename} ---\n`,
|
|
1398
|
+
text: `\n--- File: ${suppliedValue(attachment.filename)} ---\n${markedText(textContent)}\n--- End of ${suppliedValue(attachment.filename)} ---\n`,
|
|
1387
1399
|
});
|
|
1388
1400
|
} else {
|
|
1389
1401
|
// An answer whose body is never read holds its connection.
|
|
@@ -1401,11 +1413,11 @@ export class DiscordModule implements Module {
|
|
|
1401
1413
|
// For unsupported attachments, add a note
|
|
1402
1414
|
blocks.push({
|
|
1403
1415
|
type: 'text',
|
|
1404
|
-
text: `[
|
|
1416
|
+
text: `[${NOTICE_TAG.attachment}: ${suppliedValue(attachment.filename)} (${contentType ? suppliedValue(contentType) : 'unknown type'}): this host didn't read its contents in, so they aren't in your context. If you need them, ask the sender for them in a form you can read, such as pasted text.]`,
|
|
1405
1417
|
});
|
|
1406
1418
|
}
|
|
1407
1419
|
|
|
1408
|
-
return textFirst(
|
|
1420
|
+
return textFirst(storedOwnText(message.content), blocks);
|
|
1409
1421
|
}
|
|
1410
1422
|
|
|
1411
1423
|
/**
|
|
@@ -2588,9 +2588,11 @@ function inTextUnits(text: string, hit: MatchHit): MatchHit {
|
|
|
2588
2588
|
// author filtering
|
|
2589
2589
|
// ============================================================================
|
|
2590
2590
|
|
|
2591
|
-
/** metadata.author as
|
|
2592
|
-
*
|
|
2593
|
-
*
|
|
2591
|
+
/** metadata.author as MCPL channel ingestion and DiscordModule write it
|
|
2592
|
+
* ({id, name}), and ApiModule ({name}, or the author object an api:message
|
|
2593
|
+
* event carries). Their participant is "user", or the sender's name marked
|
|
2594
|
+
* with its source unless it's the generic "user", so the plain author
|
|
2595
|
+
* name is read here. */
|
|
2594
2596
|
function authorOf(msg: StoredMessage): { id?: string; name?: string } | undefined {
|
|
2595
2597
|
const a = (msg.metadata as { author?: unknown } | undefined)?.author;
|
|
2596
2598
|
if (!a || typeof a !== 'object') return undefined;
|
|
@@ -2657,9 +2659,9 @@ function toAuthorSet(spec: AuthorSpec | undefined, field: string): Set<string> |
|
|
|
2657
2659
|
|
|
2658
2660
|
/**
|
|
2659
2661
|
* Predicate for `author`/`excludeAuthor`, or null when neither is given.
|
|
2660
|
-
* A message's identities are its metadata.author name and id
|
|
2661
|
-
* stored participant
|
|
2662
|
-
* (and anything else ingested without author metadata). Exact match after
|
|
2662
|
+
* A message's identities are its metadata.author name and id when it has
|
|
2663
|
+
* one, and otherwise its stored participant, which is what identifies the
|
|
2664
|
+
* agent's own turns (and anything else ingested without author metadata). Exact match after
|
|
2663
2665
|
* normalization, never substring: "ann" must not pull in "joanne".
|
|
2664
2666
|
*/
|
|
2665
2667
|
function buildAuthorFilter(include: AuthorSpec | undefined, exclude: AuthorSpec | undefined): ((m: StoredMessage) => boolean) | null {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** The participant a module stores for a sender's name from outside the
|
|
2
|
+
* framework: on one line, and marked with its source, so it isn't an
|
|
3
|
+
* agent's name unless a host names an agent in that form. The generic
|
|
4
|
+
* `user` stays as given, since context-manager places injections by the
|
|
5
|
+
* last message whose participant is `user`, in any capitalization. */
|
|
6
|
+
export function outsideSenderName(name: string, source: string): string {
|
|
7
|
+
const line = name.replace(/[\p{Cc}\u2028\u2029]+/gu, ' ');
|
|
8
|
+
return line.toLowerCase() === 'user' ? line : `${line} (${source})`;
|
|
9
|
+
}
|
|
@@ -81,6 +81,18 @@ export function errnoWayOn(code: string | undefined, otherwise = 'Calling again
|
|
|
81
81
|
return otherwise;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
export function errnoClause(code: string | undefined, again: string, then: string): string {
|
|
85
|
+
const lasting = `${again} won't change that; `;
|
|
86
|
+
if (code === 'ENAMETOOLONG') return `${lasting}a shorter name would get past it`;
|
|
87
|
+
const fix = hostFix(code);
|
|
88
|
+
if (fix !== undefined) {
|
|
89
|
+
return code !== undefined && PASSING.has(code)
|
|
90
|
+
? `${again} may get past it; if it fails the same way, whoever runs this host can ${fix}`
|
|
91
|
+
: `${lasting}whoever runs this host can ${fix}, and ${then}`;
|
|
92
|
+
}
|
|
93
|
+
return `${again} checks it afresh; if the same reason comes back, whoever runs this host can fix it on disk`;
|
|
94
|
+
}
|
|
95
|
+
|
|
84
96
|
const OS_ERRNO = new Map<number, string>();
|
|
85
97
|
for (const [name, number] of Object.entries(constants.errno)) if (!OS_ERRNO.has(number)) OS_ERRNO.set(number, name);
|
|
86
98
|
|
|
@@ -100,9 +112,15 @@ export function changesFiles(tool: string): boolean {
|
|
|
100
112
|
return CHANGING.has(tool);
|
|
101
113
|
}
|
|
102
114
|
|
|
103
|
-
export const
|
|
115
|
+
export const UNFOLLOWED_STEP = 'whoever runs this host can set followSymlinks: true on this mount if it leads inside the mount, mount what it leads to, or a directory that holds it, if it leads outside, or remove it if it leads nowhere';
|
|
116
|
+
|
|
117
|
+
export const MOUNT_IT = 'whoever runs this host can mount it, or a directory that holds it';
|
|
118
|
+
|
|
119
|
+
export const OUTSIDE_STEP = `nothing done through this mount reaches what it leads to, and ${MOUNT_IT}`;
|
|
104
120
|
|
|
105
|
-
export const
|
|
121
|
+
export const RESOLVED_OUTSIDE_STEP = 'nothing done through this mount reaches where it resolves, and whoever runs this host can mount that place';
|
|
122
|
+
|
|
123
|
+
export const ROOT_RECHECK = "Calling again may work; if it fails the same way, whoever runs this host can check the mount's directory and its path.";
|
|
106
124
|
|
|
107
125
|
export const AWAITS_REOPEN = "The workspace can't check its files against disk until its store is reopened, so calling again won't change that; whoever runs this host can reopen it with a restart.";
|
|
108
126
|
|
|
@@ -56,7 +56,7 @@ import type { BigIntStats } from 'node:fs';
|
|
|
56
56
|
import { lstat, open, realpath } from 'node:fs/promises';
|
|
57
57
|
import { relative, sep } from 'node:path';
|
|
58
58
|
import { refusedLanding } from './effects.js';
|
|
59
|
-
import { ROOT_RECHECK, errnoWayOn, errnoWords, onlyTheHost, systemWords } from './answers.js';
|
|
59
|
+
import { OUTSIDE_STEP, RESOLVED_OUTSIDE_STEP, ROOT_RECHECK, UNFOLLOWED_STEP, errnoWayOn, errnoWords, onlyTheHost, systemWords } from './answers.js';
|
|
60
60
|
import {
|
|
61
61
|
type CheckedDir,
|
|
62
62
|
type MountView,
|
|
@@ -135,7 +135,7 @@ export class WriteRefused extends Error {
|
|
|
135
135
|
|
|
136
136
|
const AFRESH = 'Calling again checks it afresh.';
|
|
137
137
|
|
|
138
|
-
const NAME_ITS_OWN =
|
|
138
|
+
const NAME_ITS_OWN = `Calling again won't change that; ${UNFOLLOWED_STEP}.`;
|
|
139
139
|
|
|
140
140
|
function rootWayOn(code: string | undefined): string {
|
|
141
141
|
return code === 'ENOENT' || code === 'ENOTDIR' ? onlyTheHost("put the mount's directory back at its path") : errnoWayOn(code, ROOT_RECHECK, true);
|
|
@@ -145,7 +145,8 @@ function refusedWayOn(reason: string, code?: string, hostsName = false): string
|
|
|
145
145
|
if (reason === ROOT_UNAVAILABLE) return rootWayOn(code);
|
|
146
146
|
if (code !== undefined) return errnoWayOn(code, undefined, hostsName);
|
|
147
147
|
if (reason === NOT_FOLLOWED) return NAME_ITS_OWN;
|
|
148
|
-
if (reason ===
|
|
148
|
+
if (reason === LEAVES_MOUNT) return `Calling again won't change that; ${OUTSIDE_STEP}.`;
|
|
149
|
+
if (reason === OUTSIDE_PARENT) return `Calling again won't change that; ${RESOLVED_OUTSIDE_STEP}.`;
|
|
149
150
|
if (reason === NOT_A_DIRECTORY) return "Calling again won't change that while something other than a directory stands where the path needs one; another path, or removing what stands there, would.";
|
|
150
151
|
return null;
|
|
151
152
|
}
|