@animalabs/agent-framework 0.28.0 → 0.29.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/agent.d.ts.map +1 -1
- package/dist/src/agent.js +4 -1
- 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/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/framework.d.ts +164 -36
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1120 -479
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +34 -10
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +118 -48
- 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 +170 -0
- package/dist/src/host-presentation.d.ts.map +1 -0
- package/dist/src/host-presentation.js +561 -0
- package/dist/src/host-presentation.js.map +1 -0
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- 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 +34 -6
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +131 -53
- 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-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 +180 -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 +79 -0
- package/dist/src/notice-tags.d.ts.map +1 -0
- package/dist/src/notice-tags.js +83 -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 +1 -53
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js +56 -3
- package/dist/src/silent-heartbeat.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 +3 -2
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +24 -26
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/types/agent.d.ts +23 -9
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +43 -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/package.json +2 -2
- package/src/agent.ts +4 -1
- package/src/api/mcp-server-core.ts +3 -14
- package/src/api/server.ts +7 -3
- 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/framework.ts +1132 -492
- package/src/gate/event-gate.ts +128 -47
- 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 +605 -0
- package/src/index.ts +2 -2
- package/src/line-reader.ts +58 -0
- package/src/mcpl/channel-registry.ts +137 -52
- 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-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 +142 -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 +84 -0
- package/src/reasoning-loss-notice.ts +5 -3
- package/src/silent-heartbeat.ts +4 -3
- 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 +25 -27
- package/src/types/agent.ts +23 -9
- package/src/types/framework.ts +44 -0
- package/src/types/module.ts +23 -11
package/src/mcpl/toolset-host.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* which the toolsets module gathers at its next start as a stored
|
|
25
25
|
* `afterUser` injection.
|
|
26
26
|
*/
|
|
27
|
+
import { nameToTypeBack, quotedValue } from '../supplied-text.js';
|
|
27
28
|
import { McplRequestError, type McplServerConnection, type HintRead } from './server-connection.js';
|
|
28
29
|
import type { McplServerConfig } from './types.js';
|
|
29
30
|
import { isToolAllowed } from './tool-policy.js';
|
|
@@ -73,6 +74,10 @@ export interface ToolsetHostDeps {
|
|
|
73
74
|
* where it isn't published. RFC §4.2: until its move applies, a tool
|
|
74
75
|
* keeps working where it is. */
|
|
75
76
|
published?(serverId: string, tool: string): Presence | undefined;
|
|
77
|
+
/** The host's own toolsets an agent has open (the built-in one, server
|
|
78
|
+
* tabs): each one's marker and reminder, for RFC §8.3 reminders beside the
|
|
79
|
+
* servers' toolsets. */
|
|
80
|
+
hostOpen?(agentName: string): Array<{ marker: string; reminder: () => string; dueWhenAbsent?: boolean }>;
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
/** One server's RFC-014 state and traffic. Lives as long as the server's
|
|
@@ -381,8 +386,8 @@ export class ToolsetController {
|
|
|
381
386
|
*/
|
|
382
387
|
dynamicCallRefusal(tool: string, calledAs: string): string | null {
|
|
383
388
|
if (!this.presence.active || !this.presence.tool(tool) || this.presence.shownAs(tool, 'static')) return null;
|
|
384
|
-
return `There's no tool "${calledAs}" in your tool list, so nothing ran: ${tool} is a dynamic tool of ${this.serverId}, ` +
|
|
385
|
-
`called with ${META_TOOL_NAMES.act} {server:"${this.serverId}", tool
|
|
389
|
+
return `There's no tool "${calledAs}" in your tool list, so nothing ran: ${nameToTypeBack(tool)} is a dynamic tool of ${this.serverId}, ` +
|
|
390
|
+
`called with ${META_TOOL_NAMES.act} {server:"${this.serverId}", tool:${quotedValue(tool)}, args}. ${this.presence.toolStatus(tool)}.`;
|
|
386
391
|
}
|
|
387
392
|
|
|
388
393
|
/**
|
|
@@ -621,8 +626,8 @@ export class ToolsetController {
|
|
|
621
626
|
if (!moved.length) return;
|
|
622
627
|
const cs = emptyChange();
|
|
623
628
|
cs.notes.push(...moved.map(({ name, to }) => to === 'dynamic'
|
|
624
|
-
? `${name} left your tool list: it's announced when available, and called with ${META_TOOL_NAMES.act} {server:"${this.serverId}", tool
|
|
625
|
-
: `${name} is in your tool list now, as ${this.deps.listedName(this.serverId, name)}.`));
|
|
629
|
+
? `${nameToTypeBack(name)} left your tool list: it's announced when available, and called with ${META_TOOL_NAMES.act} {server:"${this.serverId}", tool:${quotedValue(name)}, args}.`
|
|
630
|
+
: `${nameToTypeBack(name)} is in your tool list now, as ${nameToTypeBack(this.deps.listedName(this.serverId, name))}.`));
|
|
626
631
|
this.deliver(cs);
|
|
627
632
|
}
|
|
628
633
|
}
|
|
@@ -681,8 +686,9 @@ export class ToolsetHub {
|
|
|
681
686
|
* reminder due.
|
|
682
687
|
*/
|
|
683
688
|
observeCompile(agentName: string, messages: readonly unknown[]): void {
|
|
689
|
+
const host = this.deps.hostOpen?.(agentName) ?? [];
|
|
684
690
|
const open = this.openMarkers();
|
|
685
|
-
if (open.length === 0) {
|
|
691
|
+
if (open.length === 0 && host.length === 0) {
|
|
686
692
|
this.seen.delete(agentName);
|
|
687
693
|
this.due.delete(agentName);
|
|
688
694
|
return;
|
|
@@ -696,6 +702,9 @@ export class ToolsetHub {
|
|
|
696
702
|
}
|
|
697
703
|
for (const marker of [...seen]) if (!open.includes(marker)) seen.delete(marker);
|
|
698
704
|
this.seen.set(agentName, seen);
|
|
705
|
+
// A host toolset open across a restart was never seen here: it's due
|
|
706
|
+
// whenever a request doesn't carry its marker.
|
|
707
|
+
for (const h of host) if (h.dueWhenAbsent && !text.includes(h.marker)) missing = true;
|
|
699
708
|
// Due until a request carries every marker the agent has seen again: a
|
|
700
709
|
// gather that doesn't send (a preview) leaves it due for the next start.
|
|
701
710
|
if (missing) this.due.add(agentName);
|
|
@@ -705,7 +714,10 @@ export class ToolsetHub {
|
|
|
705
714
|
/** The reminder due for `agentName`, or null when none is. */
|
|
706
715
|
reminderFor(agentName: string): string | null {
|
|
707
716
|
if (!this.due.has(agentName)) return null;
|
|
708
|
-
const parts =
|
|
717
|
+
const parts = [
|
|
718
|
+
...(this.deps.hostOpen?.(agentName) ?? []).map((h) => h.reminder()),
|
|
719
|
+
...this.presenting().filter((c) => this.deps.canNotify(c.serverId)).map((c) => c.presence.reminder()).filter((r): r is string => !!r),
|
|
720
|
+
];
|
|
709
721
|
if (parts.length === 0) return null;
|
|
710
722
|
return `Open toolsets (a reminder: their earlier announcements have left your context):\n${parts.join('\n')}`;
|
|
711
723
|
}
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* results, in the results of calls that changed them, in push events and in
|
|
14
14
|
* notices) and called through `toolsets--act`, which this module only
|
|
15
15
|
* resolves (resolveAct): the framework dispatches the call it resolves to
|
|
16
|
-
* down the same path as a direct call of the server's tool. The module is registered
|
|
17
|
-
*
|
|
16
|
+
* down the same path as a direct call of the server's tool. The module is registered at create(),
|
|
17
|
+
* for every agent (the host's own toolsets use it too), unless a module given to create() holds
|
|
18
|
+
* the name; its tools never change after that.
|
|
18
19
|
*
|
|
19
20
|
* Its gatherContext publishes the RFC §8.3 reminder, as a stored `afterUser`
|
|
20
21
|
* injection, when an agent's earlier announcements of an open toolset have
|
|
@@ -22,6 +23,7 @@
|
|
|
22
23
|
*
|
|
23
24
|
* Ported from mcpl-cc-bridge's meta-tools.ts, the reference host.
|
|
24
25
|
*/
|
|
26
|
+
import { nameToTypeBack, quotedValue, suppliedValue } from '../supplied-text.js';
|
|
25
27
|
import type { EventResponse, Module, ModuleContext, ModuleContextInjection, ProcessState } from '../types/module.js';
|
|
26
28
|
import type { ProcessEvent, ToolCall, ToolDefinition, ToolResult } from '../types/events.js';
|
|
27
29
|
import { unknownToolMessage } from '../modules/unknown-tool.js';
|
|
@@ -29,6 +31,7 @@ import { META_TOOL_NAMES, TOOLSETS_MODULE, type ToolsetController, type ToolsetH
|
|
|
29
31
|
import { keyOf, type ChangeSet, type ToolPresence } from './tool-presence.js';
|
|
30
32
|
import { checkArgs, jsonText, renderToolDefinition, type HintedTool } from './tool-signature.js';
|
|
31
33
|
import { McplRequestError } from './server-connection.js';
|
|
34
|
+
import { BUILTIN_TOOLSET, type HostPresentation } from '../host-presentation.js';
|
|
32
35
|
|
|
33
36
|
/** The namespace a reminder's stored injection carries. */
|
|
34
37
|
export const TOOLSETS_REMINDER_NAMESPACE = 'mcpl-toolsets';
|
|
@@ -37,24 +40,26 @@ const META_TOOLS: ToolDefinition[] = [
|
|
|
37
40
|
{
|
|
38
41
|
name: 'open',
|
|
39
42
|
description:
|
|
40
|
-
'Open a toolset
|
|
41
|
-
'
|
|
42
|
-
'
|
|
43
|
+
'Open a toolset so its tools can be called with toolsets--act: the host\'s own settings and controls (toolset "settings"), ' +
|
|
44
|
+
'an MCPL server\'s tab (server alone: its tools without putting them in your tool list; a closed tab is connected again; ' +
|
|
45
|
+
'full: true puts the server back in your tool list), or a toolset an MCPL server declares (a game, a building mode, a card table: ' +
|
|
46
|
+
'server and toolset). Returns the tools available now. instance names a separate copy where a toolset allows several; ' +
|
|
43
47
|
'options as toolsets--look lists them.',
|
|
44
48
|
inputSchema: {
|
|
45
49
|
type: 'object',
|
|
46
50
|
properties: {
|
|
47
51
|
server: { type: 'string', description: 'the MCPL server id' },
|
|
48
|
-
toolset: { type: 'string' },
|
|
52
|
+
toolset: { type: 'string', description: '"settings" for the host\'s own; a server\'s toolset with server' },
|
|
49
53
|
instance: { type: 'string', description: 'a name for this copy (toolsets with instances only)' },
|
|
50
54
|
options: { type: 'object', description: 'toolset options, as toolsets--look shows them' },
|
|
55
|
+
full: { type: 'boolean', description: 'with server alone: put the server\'s tools back in your tool list' },
|
|
51
56
|
},
|
|
52
57
|
},
|
|
53
58
|
},
|
|
54
59
|
{
|
|
55
60
|
name: 'act',
|
|
56
61
|
description:
|
|
57
|
-
'Call a tool
|
|
62
|
+
'Call a tool of an open toolset (the host\'s "settings", a server\'s tab, or a server\'s own toolset) or another dynamic tool, as announced in the conversation; they are not in your tool list. ' +
|
|
58
63
|
'args are the arguments its signature shows; instance picks the open copy when several are open; server is needed only when two servers have the tool. ' +
|
|
59
64
|
'Returns the server\'s result, then any change it made in which tools are available, unless that change already went out as a notice. toolsets--look shows what is available now.',
|
|
60
65
|
inputSchema: {
|
|
@@ -71,7 +76,7 @@ const META_TOOLS: ToolDefinition[] = [
|
|
|
71
76
|
{
|
|
72
77
|
name: 'look',
|
|
73
78
|
description:
|
|
74
|
-
'Show what is open and what can be opened
|
|
79
|
+
'Show what is open and what can be opened (the host\'s "settings", each MCPL server\'s tab, servers\' own toolsets), with the tools usable now: announcements earlier in the conversation may be out of date. ' +
|
|
75
80
|
'With tool: that tool\'s full description and input schema. With toolset or instance: just that one.',
|
|
76
81
|
inputSchema: {
|
|
77
82
|
type: 'object',
|
|
@@ -85,13 +90,15 @@ const META_TOOLS: ToolDefinition[] = [
|
|
|
85
90
|
},
|
|
86
91
|
{
|
|
87
92
|
name: 'close',
|
|
88
|
-
description: 'Close an open toolset or instance: its tools stop, and for a toolset the server declares, the server is told and stops its events too. Closing a game in progress may count as leaving it.'
|
|
93
|
+
description: 'Close an open toolset or instance: its tools stop, and for a toolset the server declares, the server is told and stops its events too. Closing a game in progress may count as leaving it. ' +
|
|
94
|
+
'server alone moves a server out of your tool list into a tab (connected, its events still come), or hides its open tab; with disconnect: true it is disconnected until you open it again.',
|
|
89
95
|
inputSchema: {
|
|
90
96
|
type: 'object',
|
|
91
97
|
properties: {
|
|
92
98
|
server: { type: 'string', description: 'the MCPL server id (may be left out when one server has the toolset open)' },
|
|
93
|
-
toolset: { type: 'string', description: 'may be left out when only one thing is open on the server' },
|
|
99
|
+
toolset: { type: 'string', description: '"settings" for the host\'s own; may be left out when only one thing is open on the server' },
|
|
94
100
|
instance: { type: 'string' },
|
|
101
|
+
disconnect: { type: 'boolean', description: 'with server alone: disconnect it as a closed tab until it is opened again' },
|
|
95
102
|
},
|
|
96
103
|
},
|
|
97
104
|
},
|
|
@@ -99,6 +106,21 @@ const META_TOOLS: ToolDefinition[] = [
|
|
|
99
106
|
|
|
100
107
|
const str = (v: unknown): string | undefined => (typeof v === 'string' && v.trim() ? v.trim() : undefined);
|
|
101
108
|
|
|
109
|
+
const selector = (v: unknown, has: (name: string) => boolean): string | undefined =>
|
|
110
|
+
typeof v === 'string' && v !== v.trim() && v.trim() !== '' && has(v) ? v : str(v);
|
|
111
|
+
const presentersOf = (hub: ToolsetHub, server: unknown): ToolsetController[] => {
|
|
112
|
+
const id = str(server);
|
|
113
|
+
if (!id) return hub.presenting();
|
|
114
|
+
const c = hub.get(id);
|
|
115
|
+
return c ? [c] : [];
|
|
116
|
+
};
|
|
117
|
+
const toolNamed = (hub: ToolsetHub, server: unknown, v: unknown): string | undefined =>
|
|
118
|
+
selector(v, (name) => presentersOf(hub, server).some((c) => !!c.presence.tool(name)));
|
|
119
|
+
const toolsetNamed = (hub: ToolsetHub, server: unknown, v: unknown): string | undefined =>
|
|
120
|
+
selector(v, (name) => presentersOf(hub, server).some((c) => c.presence.isDeclared(name) || c.presence.undeclaredGroups.includes(name)));
|
|
121
|
+
const instanceNamed = (presenters: readonly ToolsetController[], toolset: string | undefined, v: unknown): string | undefined =>
|
|
122
|
+
selector(v, (name) => presenters.some((c) => c.presence.openRecords(toolset).some((o) => o.instance === name)));
|
|
123
|
+
|
|
102
124
|
const SELECTORS = ['server', 'toolset', 'instance', 'tool'] as const;
|
|
103
125
|
|
|
104
126
|
function checkSelectors(args: Record<string, unknown>): void {
|
|
@@ -117,7 +139,7 @@ export function withLookHint(serverId: string, text: string): string {
|
|
|
117
139
|
}
|
|
118
140
|
|
|
119
141
|
function requestFailure(serverId: string, did: string, unconfirmed: string, error: unknown): never {
|
|
120
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
142
|
+
const message = suppliedValue(error instanceof Error ? error.message : String(error));
|
|
121
143
|
if (error instanceof McplRequestError && error.outcome === 'not-sent') return fail(`${serverId} ${did}: ${message}`);
|
|
122
144
|
return fail(withLookHint(serverId, `${serverId} ${unconfirmed}: ${message}`));
|
|
123
145
|
}
|
|
@@ -194,7 +216,7 @@ const NONE_PRESENTS = 'no MCPL server presents toolsets now: MCPL tools are call
|
|
|
194
216
|
|
|
195
217
|
function noToolset(c: ToolsetController, toolset: string): string {
|
|
196
218
|
const names = [...Object.keys(c.presence.declared), ...c.presence.undeclaredGroups];
|
|
197
|
-
return `${c.serverId} has no toolset "${toolset}"; ${names.length ? `it has ${names.join(', ')}` : 'it has none'}`;
|
|
219
|
+
return `${c.serverId} has no toolset "${toolset}"; ${names.length ? `it has ${names.map(nameToTypeBack).join(', ')}` : 'it has none'}`;
|
|
198
220
|
}
|
|
199
221
|
|
|
200
222
|
/** What a `toolsets--act` call resolves to: the server's tool, its instance and arguments. */
|
|
@@ -221,35 +243,35 @@ export function resolveAct(hub: ToolsetHub, input: unknown): ActTarget {
|
|
|
221
243
|
try {
|
|
222
244
|
const args = isObj(input) ? input : {};
|
|
223
245
|
checkSelectors(args);
|
|
224
|
-
const tool =
|
|
246
|
+
const tool = toolNamed(hub, args.server, args.tool) ?? fail('name the tool');
|
|
225
247
|
const c = owner(hub, args.server, (x) => !!x.presence.tool(tool), `a tool "${tool}"`);
|
|
226
248
|
const p = c.presence;
|
|
227
249
|
if (!p.tool(tool)) fail(`${c.serverId} has no tool "${tool}": ${META_TOOL_NAMES.look} {server:"${c.serverId}"} shows what's available`);
|
|
228
|
-
if (!p.shownAs(tool, 'dynamic')) fail(`${tool} is not a dynamic tool: ${p.toolStatus(tool)}. Call it there.`);
|
|
250
|
+
if (!p.shownAs(tool, 'dynamic')) fail(`${nameToTypeBack(tool)} is not a dynamic tool: ${p.toolStatus(tool)}. Call it there.`);
|
|
229
251
|
if (args.args !== undefined && !isObj(args.args)) fail('args must be an object of the tool\'s arguments');
|
|
230
252
|
const callArgs = (args.args ?? {}) as Record<string, unknown>;
|
|
231
|
-
let instance = str(args.instance);
|
|
232
253
|
const ts = p.toolsetOf(tool);
|
|
254
|
+
let instance = instanceNamed([c], ts, args.instance);
|
|
233
255
|
const declared = ts !== undefined && p.isDeclared(ts);
|
|
234
256
|
const withInstances = hasInstances(p, ts);
|
|
235
257
|
if (withInstances && !instance) {
|
|
236
258
|
// RFC §5.5: the instance rides in _meta; with exactly one open, it's that one.
|
|
237
259
|
const open = p.openRecords(ts);
|
|
238
260
|
if (open.length === 1) instance = open[0]!.instance;
|
|
239
|
-
else if (!open.length) fail(`${tool} belongs to ${ts}, which isn't open: ${p.openHint(ts!)}`);
|
|
240
|
-
else fail(`several ${ts} instances are open (${open.map((o) => o.instance).join(', ')}): say which with instance`);
|
|
261
|
+
else if (!open.length) fail(`${nameToTypeBack(tool)} belongs to ${nameToTypeBack(ts!)}, which isn't open: ${p.openHint(ts!)}`);
|
|
262
|
+
else fail(`several ${nameToTypeBack(ts!)} instances are open (${open.map((o) => nameToTypeBack(o.instance!)).join(', ')}): say which with instance`);
|
|
241
263
|
}
|
|
242
|
-
if (instance && !withInstances) fail(`${tool} ${ts ? `belongs to ${ts}, which has no instances` : 'belongs to no toolset'}: call it without instance`);
|
|
264
|
+
if (instance && !withInstances) fail(`${nameToTypeBack(tool)} ${ts ? `belongs to ${nameToTypeBack(ts)}, which has no instances` : 'belongs to no toolset'}: call it without instance`);
|
|
243
265
|
// RFC §5.4: the host gates calls into an undeclared group it has closed.
|
|
244
|
-
if (ts && !declared && !p.isOpen(ts)) fail(`${tool} is in ${ts}, which isn't open: ${p.openHint(ts)}`);
|
|
266
|
+
if (ts && !declared && !p.isOpen(ts)) fail(`${nameToTypeBack(tool)} is in ${nameToTypeBack(ts)}, which isn't open: ${p.openHint(ts)}`);
|
|
245
267
|
// RFC §6.1: arguments are checked against the definition in force for
|
|
246
268
|
// that instance: its own from hints (the legal moves of one game), else
|
|
247
269
|
// the toolset's, else the listed one.
|
|
248
270
|
const key = actKey(p, tool, instance);
|
|
249
271
|
const problem = checkArgs(p.defOf(key, tool).inputSchema, callArgs);
|
|
250
272
|
if (problem) {
|
|
251
|
-
fail(`Invalid arguments for ${tool} in ${p.label(key)}: ${problem}. Nothing was sent; ` +
|
|
252
|
-
`${META_TOOL_NAMES.look} {server:"${c.serverId}", tool
|
|
273
|
+
fail(`Invalid arguments for ${nameToTypeBack(tool)} in ${p.label(key)}: ${problem}. Nothing was sent; ` +
|
|
274
|
+
`${META_TOOL_NAMES.look} {server:"${c.serverId}", tool:${quotedValue(tool)}${instance ? `, instance:${quotedValue(instance)}` : ''}} shows its current definition.`);
|
|
253
275
|
}
|
|
254
276
|
return { ok: true, serverId: c.serverId, tool, ...(instance ? { instance } : {}), args: callArgs };
|
|
255
277
|
} catch (error) {
|
|
@@ -267,6 +289,8 @@ export class ToolsetsModule implements Module {
|
|
|
267
289
|
constructor(
|
|
268
290
|
private readonly hub: ToolsetHub,
|
|
269
291
|
private readonly takerFor: (call: ToolCall) => ((taken: TakenChange) => void) | null,
|
|
292
|
+
/** The host's own toolsets (the built-in "settings" and server tabs), when the framework presents them. */
|
|
293
|
+
private readonly host: HostPresentation | null = null,
|
|
270
294
|
) {}
|
|
271
295
|
|
|
272
296
|
async start(_ctx: ModuleContext): Promise<void> {}
|
|
@@ -291,7 +315,16 @@ export class ToolsetsModule implements Module {
|
|
|
291
315
|
const args = isObj(call.input) ? call.input : {};
|
|
292
316
|
try {
|
|
293
317
|
checkSelectors(args);
|
|
318
|
+
const hosted = await this.hostCall(call.name, args, call.callerAgentName);
|
|
319
|
+
if (hosted) return hosted;
|
|
294
320
|
const take = this.takerFor(call);
|
|
321
|
+
const lead = call.name === 'look' ? this.hostLookLead(args, call.callerAgentName) : null;
|
|
322
|
+
if (lead !== null) {
|
|
323
|
+
if (!this.hub.presenting().length) return textResult(lead);
|
|
324
|
+
const theirs = await this.look(args, take);
|
|
325
|
+
if (theirs.isError || typeof theirs.data !== 'string') return theirs;
|
|
326
|
+
return textResult(`${lead}\n\n${theirs.data}`);
|
|
327
|
+
}
|
|
295
328
|
switch (call.name) {
|
|
296
329
|
case 'open': return await this.open(args, take);
|
|
297
330
|
case 'act': return this.act(args);
|
|
@@ -307,6 +340,62 @@ export class ToolsetsModule implements Module {
|
|
|
307
340
|
}
|
|
308
341
|
}
|
|
309
342
|
|
|
343
|
+
/**
|
|
344
|
+
* The host's own toolsets: `toolset: "settings"` alone, or `server` alone
|
|
345
|
+
* (a server's tab), for open, close and look; a tool a host toolset has, for
|
|
346
|
+
* look. Null for everything else, which the servers' toolsets answer.
|
|
347
|
+
*/
|
|
348
|
+
private async hostCall(name: string, args: Record<string, unknown>, agentName: string | undefined): Promise<ToolResult | null> {
|
|
349
|
+
const host = this.host;
|
|
350
|
+
if (!host || !agentName || !host.active(agentName)) return null;
|
|
351
|
+
const server = str(args.server);
|
|
352
|
+
const toolset = str(args.toolset);
|
|
353
|
+
const instance = str(args.instance);
|
|
354
|
+
const tool = str(args.tool);
|
|
355
|
+
const builtin = toolset === BUILTIN_TOOLSET && !server && !instance;
|
|
356
|
+
const tab = !!server && !toolset && !instance && args.options === undefined;
|
|
357
|
+
if (args.full !== undefined && typeof args.full !== 'boolean') fail('full must be true or false');
|
|
358
|
+
if (args.disconnect !== undefined && typeof args.disconnect !== 'boolean') fail('disconnect must be true or false');
|
|
359
|
+
if (tab && !host.knows(server!) && !this.hub.get(server!)) fail(`no MCPL server "${server}" is configured here`);
|
|
360
|
+
switch (name) {
|
|
361
|
+
case 'open':
|
|
362
|
+
if (builtin) return textResult(host.openBuiltin(agentName));
|
|
363
|
+
if (tab) return textResult(await host.openServer(agentName, server!, { full: args.full === true }));
|
|
364
|
+
if (args.full !== undefined) fail('full goes with server alone');
|
|
365
|
+
return null;
|
|
366
|
+
case 'close':
|
|
367
|
+
if (builtin) return textResult(host.closeBuiltin(agentName));
|
|
368
|
+
if (tab) return textResult(await host.closeServer(agentName, server!, { disconnect: args.disconnect === true }));
|
|
369
|
+
if (args.disconnect !== undefined) fail('disconnect goes with server alone');
|
|
370
|
+
return null;
|
|
371
|
+
case 'look': {
|
|
372
|
+
// A host toolset's tool, with or without its server named (as act takes it).
|
|
373
|
+
if (tool && !toolset && !instance) {
|
|
374
|
+
const shown = host.lookTool(agentName, tool, server);
|
|
375
|
+
if (shown) return textResult(shown);
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
if (builtin) return textResult(host.lookToolset(agentName, { builtin: true }));
|
|
379
|
+
// A server's tab, and every host toolset with no selector, go first in
|
|
380
|
+
// what the servers' own look says (handleToolCall adds them).
|
|
381
|
+
if (tab && !tool && !this.hub.get(server!)?.presents) return textResult(host.lookToolset(agentName, { serverId: server! }));
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
default:
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** What the host's own toolsets add ahead of the servers' look: everything, or one presenting server's tab. */
|
|
390
|
+
private hostLookLead(args: Record<string, unknown>, agentName: string | undefined): string | null {
|
|
391
|
+
const host = this.host;
|
|
392
|
+
if (!host || !agentName || !host.active(agentName)) return null;
|
|
393
|
+
const server = str(args.server);
|
|
394
|
+
if (str(args.toolset) || str(args.instance) || str(args.tool)) return null;
|
|
395
|
+
if (!server) return host.catalog(agentName).join('\n');
|
|
396
|
+
return host.knows(server) ? host.lookToolset(agentName, { serverId: server }) : null;
|
|
397
|
+
}
|
|
398
|
+
|
|
310
399
|
private toolsAt(c: ToolsetController, key: string): HintedTool[] {
|
|
311
400
|
return [...(c.presence.view().get(key)?.tools.values() ?? [])];
|
|
312
401
|
}
|
|
@@ -320,36 +409,36 @@ export class ToolsetsModule implements Module {
|
|
|
320
409
|
|
|
321
410
|
private async open(args: Record<string, unknown>, take: ((taken: TakenChange) => void) | null): Promise<ToolResult> {
|
|
322
411
|
const shown = take !== null;
|
|
323
|
-
let toolset =
|
|
412
|
+
let toolset = toolsetNamed(this.hub, args.server, args.toolset);
|
|
324
413
|
const c = toolset && !str(args.server)
|
|
325
414
|
? owner(this.hub, undefined, (x) => x.presence.isDeclared(toolset!) || x.presence.undeclaredGroups.includes(toolset!), `a toolset "${toolset}"`)
|
|
326
415
|
: pick(this.hub, args.server);
|
|
327
416
|
const p = c.presence;
|
|
328
417
|
const openable = [...Object.keys(p.declared), ...p.undeclaredGroups];
|
|
329
418
|
if (!toolset) {
|
|
330
|
-
if (openable.length !== 1) fail(`${c.serverId} has ${openable.length ? `several toolsets: name one of ${openable.join(', ')}` : 'no toolsets'}`);
|
|
419
|
+
if (openable.length !== 1) fail(`${c.serverId} has ${openable.length ? `several toolsets: name one of ${openable.map(nameToTypeBack).join(', ')}` : 'no toolsets'}`);
|
|
331
420
|
toolset = openable[0]!;
|
|
332
421
|
}
|
|
333
|
-
const instance =
|
|
422
|
+
const instance = instanceNamed([c], toolset, args.instance);
|
|
334
423
|
if (args.options !== undefined && !isObj(args.options)) fail('options must be an object');
|
|
335
424
|
const options = args.options as Record<string, unknown> | undefined;
|
|
336
425
|
|
|
337
426
|
if (p.isDeclared(toolset)) {
|
|
338
|
-
if (instance && !p.declared[toolset]!.instances) fail(`${c.serverId}/${toolset} has no instances: open it without one`);
|
|
427
|
+
if (instance && !p.declared[toolset]!.instances) fail(`${c.serverId}/${nameToTypeBack(toolset)} has no instances: open it without one`);
|
|
339
428
|
let opened: Awaited<ReturnType<ToolsetController['open']>>;
|
|
340
429
|
try {
|
|
341
430
|
opened = await c.open(toolset, instance, options, { shown });
|
|
342
431
|
} catch (error) {
|
|
343
|
-
return requestFailure(c.serverId, `did not open ${toolset}`, `didn't confirm opening ${toolset}`, error);
|
|
432
|
+
return requestFailure(c.serverId, `did not open ${nameToTypeBack(toolset)}`, `didn't confirm opening ${nameToTypeBack(toolset)}`, error);
|
|
344
433
|
}
|
|
345
434
|
// RFC §5.2: opening what is open adopts it; its content says which options weren't applied.
|
|
346
435
|
const blocks: Block[] = [
|
|
347
|
-
{ type: 'text', text: `${p.label(opened.key)}${opened.title ? ` ${opened.title}` : ''}${opened.adopted ? ' (already open)' : ''}` },
|
|
436
|
+
{ type: 'text', text: `${p.label(opened.key)}${opened.title ? ` ${suppliedValue(opened.title)}` : ''}${opened.adopted ? ' (already open)' : ''}` },
|
|
348
437
|
...asBlocks(opened.content),
|
|
349
438
|
];
|
|
350
439
|
const tail = [...p.toolBlock(opened.key, this.toolsAt(c, opened.key), { headed: false, describeAll: !shown, seen: shown ? undefined : new Set<string>() })];
|
|
351
440
|
const gone = opened.change.changes.find((ch) => ch.key === opened.key)?.gone ?? [];
|
|
352
|
-
if (gone.length) tail.push(`now unavailable: ${gone.join(', ')}`);
|
|
441
|
+
if (gone.length) tail.push(`now unavailable: ${gone.map(suppliedValue).join(', ')}`);
|
|
353
442
|
const others = this.renderOthers(c, opened.change, opened.key, shown);
|
|
354
443
|
if (others) tail.push(others);
|
|
355
444
|
const close = p.closeHint(opened.key);
|
|
@@ -360,7 +449,7 @@ export class ToolsetsModule implements Module {
|
|
|
360
449
|
}
|
|
361
450
|
if (p.undeclaredGroups.includes(toolset)) {
|
|
362
451
|
// RFC §5.4: a grouping the server offers but doesn't track: the host opens it, the server isn't told.
|
|
363
|
-
if (instance || options) fail(`${c.serverId}/${toolset} takes no instance or options (the server doesn't declare it)`);
|
|
452
|
+
if (instance || options) fail(`${c.serverId}/${nameToTypeBack(toolset)} takes no instance or options (the server doesn't declare it)`);
|
|
364
453
|
const cs = c.setGroupOpen(toolset, true);
|
|
365
454
|
const key = keyOf(toolset);
|
|
366
455
|
const lines = [`${p.label(key)} open.`, ...p.toolBlock(key, this.toolsAt(c, key), { headed: false, describeAll: !shown, seen: shown ? undefined : new Set<string>() })];
|
|
@@ -400,7 +489,7 @@ export class ToolsetsModule implements Module {
|
|
|
400
489
|
};
|
|
401
490
|
let result: ToolResult;
|
|
402
491
|
try {
|
|
403
|
-
result = this.lookNow(args, server,
|
|
492
|
+
result = this.lookNow(args, server, toolNamed(this.hub, server, args.tool), shown);
|
|
404
493
|
} catch (error) {
|
|
405
494
|
giveBack();
|
|
406
495
|
if (error instanceof MetaError && unread.length) fail([error.message, ...unread].join('\n'));
|
|
@@ -426,13 +515,16 @@ export class ToolsetsModule implements Module {
|
|
|
426
515
|
|
|
427
516
|
private lookNow(args: Record<string, unknown>, server: string | undefined, tool: string | undefined, mark: boolean): ToolResult {
|
|
428
517
|
if (!server) {
|
|
429
|
-
if (tool)
|
|
430
|
-
|
|
431
|
-
|
|
518
|
+
if (tool) {
|
|
519
|
+
const c = owner(this.hub, undefined, (x) => !!x.presence.tool(tool), `a tool "${tool}"`);
|
|
520
|
+
return this.lookTool(c, tool, instanceNamed([c], c.presence.toolsetOf(tool), args.instance));
|
|
521
|
+
}
|
|
522
|
+
const ts = toolsetNamed(this.hub, undefined, args.toolset);
|
|
523
|
+
const inst = instanceNamed(this.hub.presenting(), ts, args.instance);
|
|
432
524
|
if (ts || inst) {
|
|
433
525
|
const c = owner(this.hub,
|
|
434
526
|
undefined,
|
|
435
|
-
(x) => (ts ? x.presence.isDeclared(ts) || x.presence.undeclaredGroups.includes(ts) : true) && (!inst || x.presence.openRecords().some((o) => o.instance === inst)),
|
|
527
|
+
(x) => (ts ? x.presence.isDeclared(ts) || x.presence.undeclaredGroups.includes(ts) : true) && (!inst || x.presence.openRecords(ts).some((o) => o.instance === inst)),
|
|
436
528
|
ts ? `a toolset "${ts}"${inst ? ` with instance "${inst}"` : ''}` : `an open instance "${inst}"`,
|
|
437
529
|
);
|
|
438
530
|
return this.lookNow(args, c.serverId, tool, mark);
|
|
@@ -442,14 +534,14 @@ export class ToolsetsModule implements Module {
|
|
|
442
534
|
return textResult(all.map((c) => c.presence.catalog({ mark })).join('\n\n'));
|
|
443
535
|
}
|
|
444
536
|
const c = pick(this.hub, server);
|
|
445
|
-
if (tool) return this.lookTool(c, tool,
|
|
537
|
+
if (tool) return this.lookTool(c, tool, instanceNamed([c], c.presence.toolsetOf(tool), args.instance));
|
|
446
538
|
const p = c.presence;
|
|
447
|
-
let toolset =
|
|
448
|
-
const instance =
|
|
539
|
+
let toolset = toolsetNamed(this.hub, server, args.toolset);
|
|
540
|
+
const instance = instanceNamed([c], toolset, args.instance);
|
|
449
541
|
if (!toolset && instance) {
|
|
450
542
|
const open = p.openRecords().filter((o) => o.instance !== undefined);
|
|
451
543
|
toolset = open.find((o) => o.instance === instance)?.toolset
|
|
452
|
-
?? fail(`no open instance "${instance}" on ${c.serverId}; ${open.length ? `open: ${open.map((o) => `${o.toolset}:${o.instance}`).join(', ')}` : 'no instance is open there'}`);
|
|
544
|
+
?? fail(`no open instance "${instance}" on ${c.serverId}; ${open.length ? `open: ${open.map((o) => `${nameToTypeBack(o.toolset)}:${nameToTypeBack(String(o.instance))}`).join(', ')}` : 'no instance is open there'}`);
|
|
453
545
|
}
|
|
454
546
|
if (!toolset) return textResult(p.catalog({ mark }));
|
|
455
547
|
if (p.isOpen(toolset, instance) || (!instance && p.openRecords(toolset).length === 1)) {
|
|
@@ -464,8 +556,8 @@ export class ToolsetsModule implements Module {
|
|
|
464
556
|
if (d) {
|
|
465
557
|
const open = p.openRecords(toolset);
|
|
466
558
|
return textResult([
|
|
467
|
-
`${c.serverId}/${toolset}${d.title ? ` — ${d.title}` : ''}${d.summary ? `: ${d.summary}` : ''}`,
|
|
468
|
-
open.length ? `open: ${open.map((o) => o.instance ?? toolset).join(', ')}` : 'not open',
|
|
559
|
+
`${c.serverId}/${nameToTypeBack(toolset)}${d.title ? ` — ${suppliedValue(d.title)}` : ''}${d.summary ? `: ${suppliedValue(d.summary)}` : ''}`,
|
|
560
|
+
open.length ? `open: ${open.map((o) => nameToTypeBack(o.instance ?? toolset)).join(', ')}` : 'not open',
|
|
469
561
|
...(d.instances ? ['several can be open at once; name each with instance'] : []),
|
|
470
562
|
...(d.options ? [`options: ${jsonText(d.options)}`] : []),
|
|
471
563
|
`${p.openHint(toolset)} opens it.`,
|
|
@@ -473,7 +565,7 @@ export class ToolsetsModule implements Module {
|
|
|
473
565
|
}
|
|
474
566
|
if (p.undeclaredGroups.includes(toolset)) {
|
|
475
567
|
const names = p.allTools.filter((t) => p.toolsetOf(t.name) === toolset).map((t) => t.name);
|
|
476
|
-
return textResult(`${c.serverId}/${toolset}: ${names.join(', ')}; not open: ${p.openHint(toolset)} opens it.`);
|
|
568
|
+
return textResult(`${c.serverId}/${nameToTypeBack(toolset)}: ${names.map(nameToTypeBack).join(', ')}; not open: ${p.openHint(toolset)} opens it.`);
|
|
477
569
|
}
|
|
478
570
|
return fail(noToolset(c, toolset!));
|
|
479
571
|
}
|
|
@@ -491,13 +583,13 @@ export class ToolsetsModule implements Module {
|
|
|
491
583
|
|
|
492
584
|
private async close(args: Record<string, unknown>, take: ((taken: TakenChange) => void) | null): Promise<ToolResult> {
|
|
493
585
|
const shown = take !== null;
|
|
494
|
-
const named =
|
|
586
|
+
const named = toolsetNamed(this.hub, args.server, args.toolset);
|
|
495
587
|
const c = named && !str(args.server)
|
|
496
|
-
? owner(this.hub, undefined, (x) => x.presence.isOpen(named,
|
|
588
|
+
? owner(this.hub, undefined, (x) => x.presence.isOpen(named, instanceNamed([x], named, args.instance)) || x.presence.openRecords(named).length > 0, `an open toolset "${named}"`)
|
|
497
589
|
: pick(this.hub, args.server);
|
|
498
590
|
const p = c.presence;
|
|
499
591
|
let toolset = named;
|
|
500
|
-
let instance =
|
|
592
|
+
let instance = instanceNamed([c], named, args.instance);
|
|
501
593
|
if (!toolset) {
|
|
502
594
|
const open = [
|
|
503
595
|
...p.openRecords().filter((o) => !instance || o.instance === instance),
|
|
@@ -505,7 +597,7 @@ export class ToolsetsModule implements Module {
|
|
|
505
597
|
];
|
|
506
598
|
if (open.length !== 1) {
|
|
507
599
|
fail(open.length
|
|
508
|
-
? `several are open on ${c.serverId}: name one of ${open.map((o) => `${o.toolset}${o.instance ? `:${o.instance}` : ''}`).join(', ')}`
|
|
600
|
+
? `several are open on ${c.serverId}: name one of ${open.map((o) => `${nameToTypeBack(o.toolset)}${o.instance ? `:${nameToTypeBack(o.instance)}` : ''}`).join(', ')}`
|
|
509
601
|
: `nothing is open on ${c.serverId}`);
|
|
510
602
|
}
|
|
511
603
|
toolset = open[0]!.toolset;
|
|
@@ -513,17 +605,17 @@ export class ToolsetsModule implements Module {
|
|
|
513
605
|
}
|
|
514
606
|
if (p.isDeclared(toolset!)) {
|
|
515
607
|
// As open refuses one: a toolset without instances is closed without one.
|
|
516
|
-
if (instance && !p.declared[toolset!]!.instances) fail(`${c.serverId}/${toolset} has no instances: close it without one`);
|
|
608
|
+
if (instance && !p.declared[toolset!]!.instances) fail(`${c.serverId}/${nameToTypeBack(toolset!)} has no instances: close it without one`);
|
|
517
609
|
if (p.declared[toolset!]!.instances && !instance) {
|
|
518
610
|
const open = p.openRecords(toolset);
|
|
519
|
-
if (open.length !== 1) fail(open.length ? `several ${toolset} instances are open (${open.map((o) => o.instance).join(', ')}): say which` : `${toolset} is not open`);
|
|
611
|
+
if (open.length !== 1) fail(open.length ? `several ${nameToTypeBack(toolset!)} instances are open (${open.map((o) => nameToTypeBack(String(o.instance))).join(', ')}): say which` : `${nameToTypeBack(toolset!)} is not open`);
|
|
520
612
|
instance = open[0]!.instance;
|
|
521
613
|
}
|
|
522
614
|
let closed: Awaited<ReturnType<ToolsetController['close']>>;
|
|
523
615
|
try {
|
|
524
616
|
closed = await c.close(toolset!, instance, { shown });
|
|
525
617
|
} catch (error) {
|
|
526
|
-
const what = `${toolset}${instance ? `:${instance}` : ''}`;
|
|
618
|
+
const what = `${nameToTypeBack(toolset!)}${instance ? `:${nameToTypeBack(instance)}` : ''}`;
|
|
527
619
|
return requestFailure(c.serverId, `did not close ${what}`, `didn't confirm closing ${what}`, error);
|
|
528
620
|
}
|
|
529
621
|
const key = keyOf(toolset!, instance);
|
|
@@ -532,8 +624,8 @@ export class ToolsetsModule implements Module {
|
|
|
532
624
|
return blocksResult([...asBlocks(closed.content), { type: 'text', text: change }]);
|
|
533
625
|
}
|
|
534
626
|
if (p.undeclaredGroups.includes(toolset!)) {
|
|
535
|
-
if (instance) fail(`${c.serverId}/${toolset} takes no instance (the server doesn't declare it)`);
|
|
536
|
-
if (!p.isOpen(toolset!)) fail(`${c.serverId}/${toolset} is not open`);
|
|
627
|
+
if (instance) fail(`${c.serverId}/${nameToTypeBack(toolset!)} takes no instance (the server doesn't declare it)`);
|
|
628
|
+
if (!p.isOpen(toolset!)) fail(`${c.serverId}/${nameToTypeBack(toolset!)} is not open`);
|
|
537
629
|
const cs = c.setGroupOpen(toolset!, false);
|
|
538
630
|
const text = p.render(cs, { mark: shown, describeAll: !shown }) || `${p.label(keyOf(toolset!))} closed.`;
|
|
539
631
|
if (take) take({ toolsets: c, cs });
|
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
|
/**
|