@animalabs/agent-framework 0.26.1 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -7
- package/dist/src/agent.d.ts +263 -22
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +531 -54
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +1 -2
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/auto-continue.d.ts +33 -0
- package/dist/src/auto-continue.d.ts.map +1 -0
- package/dist/src/auto-continue.js +35 -0
- package/dist/src/auto-continue.js.map +1 -0
- package/dist/src/code-execution/isolate/scripts.d.ts +11 -0
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +5 -1
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/framework.d.ts +829 -156
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +3906 -1008
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +309 -46
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +2117 -395
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/index.d.ts +2 -2
- package/dist/src/gate/index.d.ts.map +1 -1
- package/dist/src/gate/index.js +1 -1
- package/dist/src/gate/index.js.map +1 -1
- package/dist/src/gate/recipe-reconcile.d.ts +103 -0
- package/dist/src/gate/recipe-reconcile.d.ts.map +1 -0
- package/dist/src/gate/recipe-reconcile.js +325 -0
- package/dist/src/gate/recipe-reconcile.js.map +1 -0
- package/dist/src/gate/types.d.ts +68 -8
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/index.d.ts +6 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +433 -113
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +1152 -434
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +16 -8
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +29 -15
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts.map +1 -1
- package/dist/src/mcpl/convert-block.js +14 -2
- package/dist/src/mcpl/convert-block.js.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts +7 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.js +32 -7
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
- package/dist/src/mcpl/index.d.ts +5 -1
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js +4 -0
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/inference-router.d.ts +14 -2
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js +73 -28
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/prose-grammar.d.ts +4 -0
- package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
- package/dist/src/mcpl/prose-grammar.js +6 -0
- package/dist/src/mcpl/prose-grammar.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +3 -0
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +20 -2
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +33 -11
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/reference-fetcher.d.ts +63 -42
- package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
- package/dist/src/mcpl/reference-fetcher.js +177 -145
- package/dist/src/mcpl/reference-fetcher.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +380 -46
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +737 -78
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +82 -3
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +132 -7
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.d.ts +11 -0
- package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.js +21 -0
- package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
- package/dist/src/mcpl/tool-presence.d.ts +259 -0
- package/dist/src/mcpl/tool-presence.d.ts.map +1 -0
- package/dist/src/mcpl/tool-presence.js +628 -0
- package/dist/src/mcpl/tool-presence.js.map +1 -0
- package/dist/src/mcpl/tool-result-normalize.d.ts +9 -3
- package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
- package/dist/src/mcpl/tool-result-normalize.js +14 -6
- package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts +39 -0
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -0
- package/dist/src/mcpl/tool-signature.js +292 -0
- package/dist/src/mcpl/tool-signature.js.map +1 -0
- package/dist/src/mcpl/toolset-host.d.ts +200 -0
- package/dist/src/mcpl/toolset-host.d.ts.map +1 -0
- package/dist/src/mcpl/toolset-host.js +621 -0
- package/dist/src/mcpl/toolset-host.js.map +1 -0
- package/dist/src/mcpl/toolsets-module.d.ts +80 -0
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -0
- package/dist/src/mcpl/toolsets-module.js +431 -0
- package/dist/src/mcpl/toolsets-module.js.map +1 -0
- package/dist/src/mcpl/transport.d.ts +2 -2
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +3 -2
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +21 -1
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/mcpl/visible-content.d.ts +7 -9
- package/dist/src/mcpl/visible-content.d.ts.map +1 -1
- package/dist/src/mcpl/visible-content.js +21 -9
- package/dist/src/mcpl/visible-content.js.map +1 -1
- package/dist/src/module-registry.d.ts +23 -8
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +68 -15
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/index.js +1 -1
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/history/index.d.ts +2 -1
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +51 -17
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/history/semantic.d.ts +108 -6
- package/dist/src/modules/history/semantic.d.ts.map +1 -1
- package/dist/src/modules/history/semantic.js +273 -57
- package/dist/src/modules/history/semantic.js.map +1 -1
- package/dist/src/modules/workspace/direct-write.d.ts +181 -0
- package/dist/src/modules/workspace/direct-write.d.ts.map +1 -0
- package/dist/src/modules/workspace/direct-write.js +384 -0
- package/dist/src/modules/workspace/direct-write.js.map +1 -0
- package/dist/src/modules/workspace/edit-text.d.ts +26 -12
- package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
- package/dist/src/modules/workspace/edit-text.js +58 -28
- package/dist/src/modules/workspace/edit-text.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts +10 -0
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +1 -1
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +246 -27
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +1216 -236
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/read-page.d.ts +41 -0
- package/dist/src/modules/workspace/read-page.d.ts.map +1 -0
- package/dist/src/modules/workspace/read-page.js +213 -0
- package/dist/src/modules/workspace/read-page.js.map +1 -0
- package/dist/src/modules/workspace/result-text.d.ts +67 -0
- package/dist/src/modules/workspace/result-text.d.ts.map +1 -0
- package/dist/src/modules/workspace/result-text.js +94 -0
- package/dist/src/modules/workspace/result-text.js.map +1 -0
- package/dist/src/modules/workspace/text.d.ts +29 -12
- package/dist/src/modules/workspace/text.d.ts.map +1 -1
- package/dist/src/modules/workspace/text.js +35 -33
- package/dist/src/modules/workspace/text.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +41 -17
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/sent-rows.d.ts +59 -0
- package/dist/src/sent-rows.d.ts.map +1 -0
- package/dist/src/sent-rows.js +145 -0
- package/dist/src/sent-rows.js.map +1 -0
- package/dist/src/silent-heartbeat.d.ts +17 -10
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js.map +1 -1
- package/dist/src/stable-hash.d.ts +3 -0
- package/dist/src/stable-hash.d.ts.map +1 -0
- package/dist/src/stable-hash.js +15 -0
- package/dist/src/stable-hash.js.map +1 -0
- package/dist/src/stored-injections.d.ts +99 -0
- package/dist/src/stored-injections.d.ts.map +1 -0
- package/dist/src/stored-injections.js +106 -0
- package/dist/src/stored-injections.js.map +1 -0
- package/dist/src/text-as-stored.d.ts +4 -3
- package/dist/src/text-as-stored.d.ts.map +1 -1
- package/dist/src/text-as-stored.js +4 -3
- package/dist/src/text-as-stored.js.map +1 -1
- package/dist/src/thinking-drops.d.ts +81 -0
- package/dist/src/thinking-drops.d.ts.map +1 -0
- package/dist/src/thinking-drops.js +103 -0
- package/dist/src/thinking-drops.js.map +1 -0
- package/dist/src/tool-image-ledger.d.ts +9 -8
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js +9 -8
- package/dist/src/tool-image-ledger.js.map +1 -1
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +3 -1
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts +4 -2
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +21 -6
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tool-result-history.d.ts +186 -21
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +428 -89
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +17 -9
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +84 -37
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/types/agent.d.ts +186 -7
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +19 -2
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +24 -3
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +90 -7
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +57 -2
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +4 -3
- package/src/agent.ts +593 -60
- package/src/api/server.ts +1 -2
- package/src/auto-continue.ts +42 -0
- package/src/code-execution/isolate/scripts.ts +11 -3
- package/src/framework.ts +4004 -1026
- package/src/gate/event-gate.ts +2315 -389
- package/src/gate/index.ts +2 -1
- package/src/gate/recipe-reconcile.ts +355 -0
- package/src/gate/types.ts +70 -8
- package/src/index.ts +7 -4
- package/src/mcpl/channel-registry.ts +1220 -467
- package/src/mcpl/conversation-router.ts +34 -16
- package/src/mcpl/convert-block.ts +14 -1
- package/src/mcpl/hook-orchestrator.ts +33 -6
- package/src/mcpl/index.ts +6 -1
- package/src/mcpl/inference-router.ts +79 -34
- package/src/mcpl/prose-grammar.ts +7 -0
- package/src/mcpl/protocol-family.ts +2 -0
- package/src/mcpl/push-coalescer.ts +51 -13
- package/src/mcpl/reference-fetcher.ts +201 -162
- package/src/mcpl/references.ts +852 -78
- package/src/mcpl/server-connection.ts +145 -7
- package/src/mcpl/tool-lifecycle.ts +21 -0
- package/src/mcpl/tool-presence.ts +650 -0
- package/src/mcpl/tool-result-normalize.ts +18 -6
- package/src/mcpl/tool-signature.ts +285 -0
- package/src/mcpl/toolset-host.ts +637 -0
- package/src/mcpl/toolsets-module.ts +451 -0
- package/src/mcpl/transport.ts +3 -2
- package/src/mcpl/types.ts +22 -1
- package/src/mcpl/visible-content.ts +22 -9
- package/src/module-registry.ts +77 -24
- package/src/modules/discord/index.ts +1 -1
- package/src/modules/history/index.ts +48 -18
- package/src/modules/history/semantic.ts +308 -54
- package/src/modules/workspace/direct-write.ts +425 -0
- package/src/modules/workspace/edit-text.ts +82 -35
- package/src/modules/workspace/effects.ts +1 -1
- package/src/modules/workspace/index.ts +1274 -241
- package/src/modules/workspace/read-page.ts +222 -0
- package/src/modules/workspace/result-text.ts +119 -0
- package/src/modules/workspace/text.ts +58 -32
- package/src/modules/workspace/types.ts +41 -17
- package/src/sent-rows.ts +162 -0
- package/src/silent-heartbeat.ts +17 -10
- package/src/stable-hash.ts +14 -0
- package/src/stored-injections.ts +131 -0
- package/src/text-as-stored.ts +4 -3
- package/src/thinking-drops.ts +141 -0
- package/src/tool-image-ledger.ts +9 -8
- package/src/tool-presentation.ts +3 -1
- package/src/tool-result-guard.ts +19 -6
- package/src/tool-result-history.ts +488 -88
- package/src/tune-out/coordinator.ts +80 -37
- package/src/types/agent.ts +189 -7
- package/src/types/events.ts +19 -2
- package/src/types/framework.ts +21 -3
- package/src/types/module.ts +86 -7
- package/src/types/trace.ts +58 -2
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host's own interface to MCPL dynamic tools (RFC-014 §8.2): four tools,
|
|
3
|
+
* constant and static, aggregated across every server that presents
|
|
4
|
+
* toolsets.
|
|
5
|
+
*
|
|
6
|
+
* toolsets--open {server?, toolset?, instance?, options?}
|
|
7
|
+
* toolsets--act {server?, tool, instance?, args?}
|
|
8
|
+
* toolsets--look {server?, toolset?, instance?, tool?}
|
|
9
|
+
* toolsets--close {server?, toolset?, instance?}
|
|
10
|
+
*
|
|
11
|
+
* Dynamic tools never enter the API tool list: changing it rebuilds the
|
|
12
|
+
* prompt cache. They're announced in the conversation (in these tools'
|
|
13
|
+
* results, in the results of calls that changed them, in push events and in
|
|
14
|
+
* notices) and called through `toolsets--act`, which this module only
|
|
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 when a server is
|
|
17
|
+
* configured with `toolHints: true`; its tools never change after that.
|
|
18
|
+
*
|
|
19
|
+
* Its gatherContext publishes the RFC §8.3 reminder, as a stored `afterUser`
|
|
20
|
+
* injection, when an agent's earlier announcements of an open toolset have
|
|
21
|
+
* left its context (ToolsetHub.observeCompile).
|
|
22
|
+
*
|
|
23
|
+
* Ported from mcpl-cc-bridge's meta-tools.ts, the reference host.
|
|
24
|
+
*/
|
|
25
|
+
import type { EventResponse, Module, ModuleContext, ModuleContextInjection, ProcessState } from '../types/module.js';
|
|
26
|
+
import type { ProcessEvent, ToolCall, ToolDefinition, ToolResult } from '../types/events.js';
|
|
27
|
+
import { unknownToolMessage } from '../modules/unknown-tool.js';
|
|
28
|
+
import { META_TOOL_NAMES, TOOLSETS_MODULE, type ToolsetController, type ToolsetHub } from './toolset-host.js';
|
|
29
|
+
import { keyOf, type ChangeSet } from './tool-presence.js';
|
|
30
|
+
import { checkArgs, renderToolDefinition, type HintedTool } from './tool-signature.js';
|
|
31
|
+
|
|
32
|
+
/** The namespace a reminder's stored injection carries. */
|
|
33
|
+
export const TOOLSETS_REMINDER_NAMESPACE = 'mcpl-toolsets';
|
|
34
|
+
|
|
35
|
+
const META_TOOLS: ToolDefinition[] = [
|
|
36
|
+
{
|
|
37
|
+
name: 'open',
|
|
38
|
+
description:
|
|
39
|
+
'Open a toolset of an MCPL server (a game, a building mode, a card table) so its tools can be called with toolsets--act. ' +
|
|
40
|
+
'Returns what the server shows on opening and the tools available now. server may be left out when one server has the toolset, ' +
|
|
41
|
+
'and toolset when the server has only one; instance names a separate copy where a toolset allows several (two games at once); ' +
|
|
42
|
+
'options as toolsets--look lists them.',
|
|
43
|
+
inputSchema: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
server: { type: 'string', description: 'the MCPL server id' },
|
|
47
|
+
toolset: { type: 'string' },
|
|
48
|
+
instance: { type: 'string', description: 'a name for this copy (toolsets with instances only)' },
|
|
49
|
+
options: { type: 'object', description: 'toolset options, as toolsets--look shows them' },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'act',
|
|
55
|
+
description:
|
|
56
|
+
'Call a tool that was announced in the conversation (a tool of an open toolset, or another dynamic tool); they are not in your tool list. ' +
|
|
57
|
+
'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. ' +
|
|
58
|
+
'Returns the server\'s result, then any change in which tools are available.',
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: 'object',
|
|
61
|
+
properties: {
|
|
62
|
+
server: { type: 'string', description: 'the MCPL server id (only needed when two servers have the tool)' },
|
|
63
|
+
tool: { type: 'string' },
|
|
64
|
+
instance: { type: 'string', description: 'which open copy (only needed when several are open)' },
|
|
65
|
+
args: { type: 'object' },
|
|
66
|
+
},
|
|
67
|
+
required: ['tool'],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'look',
|
|
72
|
+
description:
|
|
73
|
+
'Show what is open and what can be opened on MCPL servers, with the tools usable now: announcements earlier in the conversation may be out of date. ' +
|
|
74
|
+
'With tool: that tool\'s full description and input schema. With toolset or instance: just that one.',
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: {
|
|
78
|
+
server: { type: 'string' },
|
|
79
|
+
toolset: { type: 'string' },
|
|
80
|
+
instance: { type: 'string' },
|
|
81
|
+
tool: { type: 'string' },
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'close',
|
|
87
|
+
description: 'Close an open toolset or instance: its tools and its events stop. Closing a game in progress may count as leaving it.',
|
|
88
|
+
inputSchema: {
|
|
89
|
+
type: 'object',
|
|
90
|
+
properties: {
|
|
91
|
+
server: { type: 'string', description: 'the MCPL server id (may be left out when one server has the toolset open)' },
|
|
92
|
+
toolset: { type: 'string', description: 'may be left out when only one thing is open on the server' },
|
|
93
|
+
instance: { type: 'string' },
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
const str = (v: unknown): string | undefined => (typeof v === 'string' && v.trim() ? v.trim() : undefined);
|
|
100
|
+
const isObj = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
101
|
+
|
|
102
|
+
type Block = { type: string; text?: string; [k: string]: unknown };
|
|
103
|
+
|
|
104
|
+
class MetaError extends Error {}
|
|
105
|
+
const fail = (message: string): never => {
|
|
106
|
+
throw new MetaError(message);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** A text-only result. */
|
|
110
|
+
const textResult = (text: string): ToolResult => ({ success: true, data: text });
|
|
111
|
+
|
|
112
|
+
/** Adjacent text blocks merged, so a result reads as one message. */
|
|
113
|
+
function mergeText(blocks: Block[]): Block[] {
|
|
114
|
+
const out: Block[] = [];
|
|
115
|
+
for (const b of blocks) {
|
|
116
|
+
if (b.type === 'text' && !b.text) continue;
|
|
117
|
+
const prev = out[out.length - 1];
|
|
118
|
+
if (b.type === 'text' && prev?.type === 'text') out[out.length - 1] = { type: 'text', text: `${prev.text}\n${b.text}` };
|
|
119
|
+
else out.push(b);
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Content blocks as a result: content when anything isn't text, else its text. */
|
|
125
|
+
function blocksResult(blocks: Block[], isError = false): ToolResult {
|
|
126
|
+
const merged = mergeText(blocks);
|
|
127
|
+
if (isError) {
|
|
128
|
+
const text = merged.map((b) => (b.type === 'text' ? b.text ?? '' : `[${b.type}]`)).join('\n');
|
|
129
|
+
return { success: false, isError: true, error: text || 'the server answered with an error' };
|
|
130
|
+
}
|
|
131
|
+
if (merged.every((b) => b.type === 'text')) return textResult(merged.map((b) => b.text ?? '').join('\n'));
|
|
132
|
+
return { success: true, data: merged, dataIsContent: true };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const asBlocks = (content: unknown[]): Block[] => content.filter((b): b is Block => isObj(b) && typeof b.type === 'string');
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
// ── choosing the server ──
|
|
139
|
+
|
|
140
|
+
function pick(hub: ToolsetHub, server: unknown): ToolsetController {
|
|
141
|
+
const id = str(server);
|
|
142
|
+
const ids = hub.presenting().map((c) => c.serverId);
|
|
143
|
+
if (!id) {
|
|
144
|
+
if (ids.length === 1) return hub.get(ids[0]!)!;
|
|
145
|
+
return fail(`name a server: ${ids.join(', ') || '(no MCPL server presents toolsets now)'}`);
|
|
146
|
+
}
|
|
147
|
+
const c = hub.get(id);
|
|
148
|
+
if (!c) return fail(`no server "${id}" presents toolsets; the ones that do: ${ids.join(', ') || 'none'}`);
|
|
149
|
+
if (!c.presence.active) return fail(`${id} sends no tool hints now: its tools are in your tool list`);
|
|
150
|
+
return c;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* RFC §8.2: `server` may be left out; the name decides, when exactly one
|
|
155
|
+
* presenting server has it. Two or more: name the server.
|
|
156
|
+
*/
|
|
157
|
+
function owner(hub: ToolsetHub, server: unknown, has: (c: ToolsetController) => boolean, what: string): ToolsetController {
|
|
158
|
+
if (str(server)) return pick(hub, server);
|
|
159
|
+
const owners = hub.presenting().filter(has);
|
|
160
|
+
if (owners.length === 1) return owners[0]!;
|
|
161
|
+
return fail(owners.length
|
|
162
|
+
? `${what} is on several servers (${owners.map((c) => c.serverId).join(', ')}): add server`
|
|
163
|
+
: `no server that presents toolsets has ${what}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** What a `toolsets--act` call resolves to: the server's tool, its instance and arguments. */
|
|
167
|
+
export type ActTarget = { ok: true; serverId: string; tool: string; instance?: string; args: Record<string, unknown> } | { ok: false; error: string };
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Resolve a `toolsets--act` call (RFC §8.2) to the dynamic tool it calls,
|
|
171
|
+
* or the reason it can't be sent. The framework then dispatches it exactly as
|
|
172
|
+
* a direct call of that tool, with `_meta["mcpl/instance"]` added (RFC §5.5).
|
|
173
|
+
* Refused here, before anything is sent: a tool no presenting server has, a
|
|
174
|
+
* static tool (it's in the tool list), an instance that isn't named where
|
|
175
|
+
* several are open, a group the host has closed (RFC §5.4), and arguments
|
|
176
|
+
* that don't fit the definition in force for that instance (RFC §6.1).
|
|
177
|
+
*/
|
|
178
|
+
export function resolveAct(hub: ToolsetHub, input: unknown): ActTarget {
|
|
179
|
+
try {
|
|
180
|
+
const args = isObj(input) ? input : {};
|
|
181
|
+
const tool = str(args.tool) ?? fail('name the tool');
|
|
182
|
+
const c = owner(hub, args.server, (x) => !!x.presence.tool(tool), `a tool "${tool}"`);
|
|
183
|
+
const p = c.presence;
|
|
184
|
+
if (!p.tool(tool)) fail(`${c.serverId} has no tool "${tool}": ${META_TOOL_NAMES.look} {server:"${c.serverId}"} shows what's available`);
|
|
185
|
+
if (p.presenceOf(tool) === 'static') fail(`${tool} is not a dynamic tool: ${p.toolStatus(tool)}. Call it there.`);
|
|
186
|
+
if (args.args !== undefined && !isObj(args.args)) fail('args must be an object of the tool\'s arguments');
|
|
187
|
+
const callArgs = (args.args ?? {}) as Record<string, unknown>;
|
|
188
|
+
let instance = str(args.instance);
|
|
189
|
+
const ts = p.toolsetOf(tool);
|
|
190
|
+
const declared = ts !== undefined && p.isDeclared(ts);
|
|
191
|
+
const withInstances = declared && !!p.declared[ts!]!.instances;
|
|
192
|
+
if (withInstances && !instance) {
|
|
193
|
+
// RFC §5.5: the instance rides in _meta; with exactly one open, it's that one.
|
|
194
|
+
const open = p.openRecords(ts);
|
|
195
|
+
if (open.length === 1) instance = open[0]!.instance;
|
|
196
|
+
else if (!open.length) fail(`${tool} belongs to ${ts}, which isn't open: ${p.openHint(ts!)}`);
|
|
197
|
+
else fail(`several ${ts} instances are open (${open.map((o) => o.instance).join(', ')}): say which with instance`);
|
|
198
|
+
}
|
|
199
|
+
if (instance && !withInstances) fail(`${tool} ${ts ? `belongs to ${ts}, which has no instances` : 'belongs to no toolset'}: call it without instance`);
|
|
200
|
+
// RFC §5.4: the host gates calls into an undeclared group it has closed.
|
|
201
|
+
if (ts && !declared && !p.isOpen(ts)) fail(`${tool} is in ${ts}, which isn't open: ${p.openHint(ts)}`);
|
|
202
|
+
// RFC §6.1: arguments are checked against the definition in force for
|
|
203
|
+
// that instance: its own from hints (the legal moves of one game), else
|
|
204
|
+
// the toolset's, else the listed one.
|
|
205
|
+
const key = ts ? keyOf(ts, withInstances ? instance : undefined) : keyOf('');
|
|
206
|
+
const problem = checkArgs(p.defOf(key, tool).inputSchema, callArgs);
|
|
207
|
+
if (problem) {
|
|
208
|
+
fail(`Invalid arguments for ${tool} in ${p.label(key)}: ${problem}. Nothing was sent; ` +
|
|
209
|
+
`${META_TOOL_NAMES.look} {server:"${c.serverId}", tool:"${tool}"${instance ? `, instance:"${instance}"` : ''}} shows its current definition.`);
|
|
210
|
+
}
|
|
211
|
+
return { ok: true, serverId: c.serverId, tool, ...(instance ? { instance } : {}), args: callArgs };
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (error instanceof MetaError) return { ok: false, error: error.message };
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export class ToolsetsModule implements Module {
|
|
219
|
+
readonly name = TOOLSETS_MODULE;
|
|
220
|
+
|
|
221
|
+
constructor(private readonly hub: ToolsetHub) {}
|
|
222
|
+
|
|
223
|
+
async start(_ctx: ModuleContext): Promise<void> {}
|
|
224
|
+
async stop(): Promise<void> {}
|
|
225
|
+
|
|
226
|
+
getTools(): ToolDefinition[] {
|
|
227
|
+
return META_TOOLS;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async onProcess(_event: ProcessEvent, _state: ProcessState): Promise<EventResponse> {
|
|
231
|
+
return {};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** RFC §8.3: the reminder due for this agent, stored where compile() would place it. */
|
|
235
|
+
async gatherContext(agentName: string): Promise<ModuleContextInjection[]> {
|
|
236
|
+
const reminder = this.hub.reminderFor(agentName);
|
|
237
|
+
if (!reminder) return [];
|
|
238
|
+
return [{ namespace: TOOLSETS_REMINDER_NAMESPACE, position: 'afterUser', content: [{ type: 'text', text: reminder }] }];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async handleToolCall(call: ToolCall): Promise<ToolResult> {
|
|
242
|
+
const args = isObj(call.input) ? call.input : {};
|
|
243
|
+
try {
|
|
244
|
+
switch (call.name) {
|
|
245
|
+
case 'open': return await this.open(args);
|
|
246
|
+
case 'act': return this.act(args);
|
|
247
|
+
case 'look': return await this.look(args);
|
|
248
|
+
case 'close': return await this.close(args);
|
|
249
|
+
default: return { success: false, isError: true, error: unknownToolMessage(this.name, call.name, META_TOOLS) };
|
|
250
|
+
}
|
|
251
|
+
} catch (error) {
|
|
252
|
+
const message = error instanceof MetaError
|
|
253
|
+
? error.message
|
|
254
|
+
: `${META_TOOL_NAMES[call.name as keyof typeof META_TOOL_NAMES] ?? call.name}: ${error instanceof Error ? error.message : String(error)}`;
|
|
255
|
+
return { success: false, isError: true, error: message };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private toolsAt(c: ToolsetController, key: string): HintedTool[] {
|
|
260
|
+
return [...(c.presence.view().get(key)?.tools.values() ?? [])];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** A change set rendered without the key the caller already showed whole. */
|
|
264
|
+
private renderOthers(c: ToolsetController, cs: ChangeSet, key: string): string {
|
|
265
|
+
return c.presence.render({ ...cs, changes: cs.changes.filter((ch) => ch.key !== key) });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── the tools ──
|
|
269
|
+
|
|
270
|
+
private async open(args: Record<string, unknown>): Promise<ToolResult> {
|
|
271
|
+
let toolset = str(args.toolset);
|
|
272
|
+
const c = toolset && !str(args.server)
|
|
273
|
+
? owner(this.hub, undefined, (x) => x.presence.isDeclared(toolset!) || x.presence.undeclaredGroups.includes(toolset!), `a toolset "${toolset}"`)
|
|
274
|
+
: pick(this.hub, args.server);
|
|
275
|
+
const p = c.presence;
|
|
276
|
+
const openable = [...Object.keys(p.declared), ...p.undeclaredGroups];
|
|
277
|
+
if (!toolset) {
|
|
278
|
+
if (openable.length !== 1) fail(`${c.serverId} has ${openable.length ? `several toolsets: name one of ${openable.join(', ')}` : 'no toolsets'}`);
|
|
279
|
+
toolset = openable[0]!;
|
|
280
|
+
}
|
|
281
|
+
const instance = str(args.instance);
|
|
282
|
+
if (args.options !== undefined && !isObj(args.options)) fail('options must be an object');
|
|
283
|
+
const options = args.options as Record<string, unknown> | undefined;
|
|
284
|
+
|
|
285
|
+
if (p.isDeclared(toolset)) {
|
|
286
|
+
if (instance && !p.declared[toolset]!.instances) fail(`${c.serverId}/${toolset} has no instances: open it without one`);
|
|
287
|
+
let opened: Awaited<ReturnType<ToolsetController['open']>>;
|
|
288
|
+
try {
|
|
289
|
+
opened = await c.open(toolset, instance, options);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
return fail(`${c.serverId} did not open ${toolset}: ${error instanceof Error ? error.message : String(error)}`);
|
|
292
|
+
}
|
|
293
|
+
// RFC §5.2: opening what is open adopts it; its content says which options weren't applied.
|
|
294
|
+
const blocks: Block[] = [
|
|
295
|
+
{ type: 'text', text: `${p.label(opened.key)}${opened.title ? ` ${opened.title}` : ''}${opened.adopted ? ' (already open)' : ''}` },
|
|
296
|
+
...asBlocks(opened.content),
|
|
297
|
+
];
|
|
298
|
+
const tail = [...p.toolBlock(opened.key, this.toolsAt(c, opened.key), { headed: false })];
|
|
299
|
+
const gone = opened.change.changes.find((ch) => ch.key === opened.key)?.gone ?? [];
|
|
300
|
+
if (gone.length) tail.push(`now unavailable: ${gone.join(', ')}`);
|
|
301
|
+
const others = this.renderOthers(c, opened.change, opened.key);
|
|
302
|
+
if (others) tail.push(others);
|
|
303
|
+
const close = p.closeHint(opened.key);
|
|
304
|
+
if (close) tail.push(`${close} closes it.`);
|
|
305
|
+
blocks.push({ type: 'text', text: tail.join('\n') });
|
|
306
|
+
return blocksResult(blocks);
|
|
307
|
+
}
|
|
308
|
+
if (p.undeclaredGroups.includes(toolset)) {
|
|
309
|
+
// RFC §5.4: a grouping the server offers but doesn't track: the host opens it, the server isn't told.
|
|
310
|
+
if (instance || options) fail(`${c.serverId}/${toolset} takes no instance or options (the server doesn't declare it)`);
|
|
311
|
+
const cs = c.setGroupOpen(toolset, true);
|
|
312
|
+
const key = keyOf(toolset);
|
|
313
|
+
const lines = [`${p.label(key)} open.`, ...p.toolBlock(key, this.toolsAt(c, key), { headed: false })];
|
|
314
|
+
const others = this.renderOthers(c, cs, key);
|
|
315
|
+
if (others) lines.push(others);
|
|
316
|
+
lines.push(`${p.closeHint(key)} closes it.`);
|
|
317
|
+
return textResult(lines.join('\n'));
|
|
318
|
+
}
|
|
319
|
+
return fail(`${c.serverId} has no toolset "${toolset}"${openable.length ? `; it has ${openable.join(', ')}` : ''}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* act is dispatched by the framework, not here: it resolves the call
|
|
324
|
+
* (resolveAct) and sends it down the same path as a direct call of the
|
|
325
|
+
* server's tool, so state, results, refusals and lifecycle are the same.
|
|
326
|
+
* A caller that reaches the module itself gets the resolution's refusal,
|
|
327
|
+
* or this.
|
|
328
|
+
*/
|
|
329
|
+
private act(args: Record<string, unknown>): ToolResult {
|
|
330
|
+
const target = resolveAct(this.hub, args);
|
|
331
|
+
if (!target.ok) return { success: false, isError: true, error: target.error };
|
|
332
|
+
return { success: false, isError: true, error: `${META_TOOL_NAMES.act} is dispatched by the framework; nothing was sent` };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** look restates the present: titles and visibility are re-read from the servers first (toolsets/list). */
|
|
336
|
+
private async look(args: Record<string, unknown>): Promise<ToolResult> {
|
|
337
|
+
const server = str(args.server);
|
|
338
|
+
const targets = server ? [this.hub.get(server)].filter((c): c is ToolsetController => !!c && c.presents) : this.hub.presenting();
|
|
339
|
+
const notes = (await Promise.all(targets.map((c) => c.refreshOpen().catch(() => [] as string[])))).flat();
|
|
340
|
+
const result = this.lookNow(args, server, str(args.tool));
|
|
341
|
+
return notes.length && !result.isError && typeof result.data === 'string' ? textResult(`${result.data}\n${notes.join('\n')}`) : result;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private lookNow(args: Record<string, unknown>, server: string | undefined, tool: string | undefined): ToolResult {
|
|
345
|
+
if (!server) {
|
|
346
|
+
if (tool) return this.lookTool(owner(this.hub, undefined, (c) => !!c.presence.tool(tool), `a tool "${tool}"`), tool, str(args.instance));
|
|
347
|
+
const ts = str(args.toolset);
|
|
348
|
+
const inst = str(args.instance);
|
|
349
|
+
if (ts || inst) {
|
|
350
|
+
const c = owner(this.hub,
|
|
351
|
+
undefined,
|
|
352
|
+
(x) => (ts ? x.presence.isDeclared(ts) || x.presence.undeclaredGroups.includes(ts) : true) && (!inst || x.presence.openRecords().some((o) => o.instance === inst)),
|
|
353
|
+
ts ? `a toolset "${ts}"${inst ? ` with instance "${inst}"` : ''}` : `an open instance "${inst}"`,
|
|
354
|
+
);
|
|
355
|
+
return this.lookNow(args, c.serverId, tool);
|
|
356
|
+
}
|
|
357
|
+
const all = this.hub.presenting();
|
|
358
|
+
if (!all.length) return textResult('No MCPL server presents toolsets or dynamic tools now; every tool is in your tool list.');
|
|
359
|
+
return textResult(all.map((c) => c.presence.catalog()).join('\n\n'));
|
|
360
|
+
}
|
|
361
|
+
const c = pick(this.hub, server);
|
|
362
|
+
if (tool) return this.lookTool(c, tool, str(args.instance));
|
|
363
|
+
const p = c.presence;
|
|
364
|
+
let toolset = str(args.toolset);
|
|
365
|
+
const instance = str(args.instance);
|
|
366
|
+
if (!toolset && instance) toolset = p.openRecords().find((o) => o.instance === instance)?.toolset ?? fail(`no open instance "${instance}" on ${c.serverId}`);
|
|
367
|
+
if (!toolset) return textResult(p.catalog());
|
|
368
|
+
if (p.isOpen(toolset, instance) || (!instance && p.openRecords(toolset).length === 1)) {
|
|
369
|
+
const rec = p.isDeclared(toolset) ? (instance ? p.openRecords(toolset).find((o) => o.instance === instance) : p.openRecords(toolset)[0]) : undefined;
|
|
370
|
+
const key = keyOf(toolset, rec?.instance);
|
|
371
|
+
const lines = p.toolBlock(key, this.toolsAt(c, key), { describeAll: true, title: rec?.title });
|
|
372
|
+
const close = p.closeHint(key);
|
|
373
|
+
if (close) lines.push(`${close} closes it.`);
|
|
374
|
+
return textResult(lines.join('\n'));
|
|
375
|
+
}
|
|
376
|
+
const d = p.declared[toolset];
|
|
377
|
+
if (d) {
|
|
378
|
+
const open = p.openRecords(toolset);
|
|
379
|
+
return textResult([
|
|
380
|
+
`${c.serverId}/${toolset}${d.title ? ` — ${d.title}` : ''}${d.summary ? `: ${d.summary}` : ''}`,
|
|
381
|
+
open.length ? `open: ${open.map((o) => o.instance ?? toolset).join(', ')}` : 'not open',
|
|
382
|
+
...(d.instances ? ['several can be open at once; name each with instance'] : []),
|
|
383
|
+
...(d.options ? [`options: ${JSON.stringify(d.options)}`] : []),
|
|
384
|
+
`${p.openHint(toolset)} opens it.`,
|
|
385
|
+
].join('\n'));
|
|
386
|
+
}
|
|
387
|
+
if (p.undeclaredGroups.includes(toolset)) {
|
|
388
|
+
const names = p.allTools.filter((t) => p.toolsetOf(t.name) === toolset).map((t) => t.name);
|
|
389
|
+
return textResult(`${c.serverId}/${toolset}: ${names.join(', ')}; not open: ${p.openHint(toolset)} opens it.`);
|
|
390
|
+
}
|
|
391
|
+
return fail(`${c.serverId} has no toolset "${toolset}"`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private lookTool(c: ToolsetController, tool: string, instance: string | undefined): ToolResult {
|
|
395
|
+
const p = c.presence;
|
|
396
|
+
if (!p.tool(tool)) fail(`${c.serverId} has no tool "${tool}"`);
|
|
397
|
+
const ts = p.toolsetOf(tool);
|
|
398
|
+
const where = [...p.view().entries()].filter(([, e]) => e.tools.has(tool)).map(([k]) => k);
|
|
399
|
+
const key = instance && ts ? keyOf(ts, instance) : where[0];
|
|
400
|
+
const def = key ? p.defOf(key, tool) : p.tool(tool)!;
|
|
401
|
+
return textResult(`${renderToolDefinition(def)}\n${p.toolStatus(tool)}`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private async close(args: Record<string, unknown>): Promise<ToolResult> {
|
|
405
|
+
const named = str(args.toolset);
|
|
406
|
+
const c = named && !str(args.server)
|
|
407
|
+
? owner(this.hub, undefined, (x) => x.presence.isOpen(named, str(args.instance)) || x.presence.openRecords(named).length > 0, `an open toolset "${named}"`)
|
|
408
|
+
: pick(this.hub, args.server);
|
|
409
|
+
const p = c.presence;
|
|
410
|
+
let toolset = named;
|
|
411
|
+
let instance = str(args.instance);
|
|
412
|
+
if (!toolset) {
|
|
413
|
+
const open = [
|
|
414
|
+
...p.openRecords().filter((o) => !instance || o.instance === instance),
|
|
415
|
+
...(instance ? [] : p.openGroups.map((g) => ({ toolset: g, instance: undefined as string | undefined }))),
|
|
416
|
+
];
|
|
417
|
+
if (open.length !== 1) {
|
|
418
|
+
fail(open.length
|
|
419
|
+
? `several are open on ${c.serverId}: name one of ${open.map((o) => `${o.toolset}${o.instance ? `:${o.instance}` : ''}`).join(', ')}`
|
|
420
|
+
: `nothing is open on ${c.serverId}`);
|
|
421
|
+
}
|
|
422
|
+
toolset = open[0]!.toolset;
|
|
423
|
+
instance = open[0]!.instance;
|
|
424
|
+
}
|
|
425
|
+
if (p.isDeclared(toolset!)) {
|
|
426
|
+
// As open refuses one: a toolset without instances is closed without one.
|
|
427
|
+
if (instance && !p.declared[toolset!]!.instances) fail(`${c.serverId}/${toolset} has no instances: close it without one`);
|
|
428
|
+
if (p.declared[toolset!]!.instances && !instance) {
|
|
429
|
+
const open = p.openRecords(toolset);
|
|
430
|
+
if (open.length !== 1) fail(open.length ? `several ${toolset} instances are open (${open.map((o) => o.instance).join(', ')}): say which` : `${toolset} is not open`);
|
|
431
|
+
instance = open[0]!.instance;
|
|
432
|
+
}
|
|
433
|
+
let closed: Awaited<ReturnType<ToolsetController['close']>>;
|
|
434
|
+
try {
|
|
435
|
+
closed = await c.close(toolset!, instance);
|
|
436
|
+
} catch (error) {
|
|
437
|
+
return fail(`${c.serverId} did not close ${toolset}${instance ? `:${instance}` : ''}: ${error instanceof Error ? error.message : String(error)}`);
|
|
438
|
+
}
|
|
439
|
+
const key = keyOf(toolset!, instance);
|
|
440
|
+
const change = p.render(closed.change) || `${p.label(key)} closed.`;
|
|
441
|
+
return blocksResult([...asBlocks(closed.content), { type: 'text', text: change }]);
|
|
442
|
+
}
|
|
443
|
+
if (p.undeclaredGroups.includes(toolset!)) {
|
|
444
|
+
if (instance) fail(`${c.serverId}/${toolset} takes no instance (the server doesn't declare it)`);
|
|
445
|
+
if (!p.isOpen(toolset!)) fail(`${c.serverId}/${toolset} is not open`);
|
|
446
|
+
const cs = c.setGroupOpen(toolset!, false);
|
|
447
|
+
return textResult(p.render(cs) || `${p.label(keyOf(toolset!))} closed.`);
|
|
448
|
+
}
|
|
449
|
+
return fail(`${c.serverId} has no toolset "${toolset}"`);
|
|
450
|
+
}
|
|
451
|
+
}
|
package/src/mcpl/transport.ts
CHANGED
|
@@ -830,11 +830,12 @@ const SERVER_CONFIG_BOOLEANS: Record<BooleanSetting, string> = {
|
|
|
830
830
|
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
831
|
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
832
|
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 the server\'s dynamic tools are announced in the conversation and called through toolsets--act, and false or leaving it out lists every tool as before',
|
|
833
834
|
};
|
|
834
835
|
|
|
835
836
|
/**
|
|
836
|
-
* Refuse a server config whose `inheritEnv`, `reconnect
|
|
837
|
-
* holds anything but true or false. A config read from JSON can carry a quoted
|
|
837
|
+
* Refuse a server config whose `inheritEnv`, `reconnect`, `allowHostCommands`
|
|
838
|
+
* or `toolHints` holds anything but true or false. A config read from JSON can carry a quoted
|
|
838
839
|
* "false" where the type says boolean, and every reader of these settings takes
|
|
839
840
|
* only `true` as on, so the quotes would hide what the value does. null counts
|
|
840
841
|
* as not set, so a generated config that writes an unset setting as null
|
package/src/mcpl/types.ts
CHANGED
|
@@ -96,6 +96,9 @@ export interface McplAudioContent {
|
|
|
96
96
|
disposition?: 'never' | 'ref';
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/** MCPL's reference block (RFC-005 §3). A `resource` block with no `uri` of
|
|
100
|
+
* its own may be MCP's embedded resource instead, its contents under
|
|
101
|
+
* `resource`, which classifyBlock tells apart and shows as its contents. */
|
|
99
102
|
export interface McplResourceContent {
|
|
100
103
|
type: 'resource';
|
|
101
104
|
/** Resource URI (e.g., "memory://facts/12345") */
|
|
@@ -190,6 +193,9 @@ export interface McplHostCapabilities {
|
|
|
190
193
|
modelInfo?: boolean;
|
|
191
194
|
featureSets?: boolean;
|
|
192
195
|
channels?: boolean | McplChannelCapabilities;
|
|
196
|
+
/** RFC-014 §6.3: this host presents tool hints. Sent only to a server
|
|
197
|
+
* whose config sets `toolHints: true`. */
|
|
198
|
+
toolHints?: boolean;
|
|
193
199
|
}
|
|
194
200
|
|
|
195
201
|
/** Channel capability leaves (§14.1). `observe` was the pre-0.5 name for
|
|
@@ -498,6 +504,20 @@ export interface McplServerConfig {
|
|
|
498
504
|
* See src/mcpl/tool-lifecycle.ts.
|
|
499
505
|
*/
|
|
500
506
|
toolLifecycle?: import('./tool-lifecycle.js').ToolLifecycleConfig;
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* MCPL RFC-014 (tool presence, toolsets and surfaces). Default false: the
|
|
510
|
+
* host advertises nothing and treats every tool as static, byte for byte as
|
|
511
|
+
* before. When true, the host advertises `experimental.mcpl.toolHints`, and
|
|
512
|
+
* honours hints only from a server that advertised `toolHints` too. Then
|
|
513
|
+
* the server's static tools stay in the tool list as before, and its
|
|
514
|
+
* dynamic tools (toolset tools, `mcpl/presence: "dynamic"`) leave it: they
|
|
515
|
+
* are announced in the conversation as they become visible and invisible,
|
|
516
|
+
* and called through the `toolsets` module's `toolsets--act`, opened with
|
|
517
|
+
* `toolsets--open`, inspected with `toolsets--look` and closed with
|
|
518
|
+
* `toolsets--close`. Only `true` turns it on, and null counts as not set.
|
|
519
|
+
*/
|
|
520
|
+
toolHints?: boolean;
|
|
501
521
|
}
|
|
502
522
|
|
|
503
523
|
// ============================================================================
|
|
@@ -1299,7 +1319,8 @@ export interface ChannelIncomingMessageResult {
|
|
|
1299
1319
|
* Non-normative; host-defined. See Spec Section 11.5.
|
|
1300
1320
|
*/
|
|
1301
1321
|
export interface InferenceRoutingPolicy {
|
|
1302
|
-
/**
|
|
1322
|
+
/** Model for a request no override, byFeature or wildcards entry names a
|
|
1323
|
+
* model for. Left empty, such a request is refused with -32603. */
|
|
1303
1324
|
default: string;
|
|
1304
1325
|
|
|
1305
1326
|
/** Model override per feature set name */
|
|
@@ -14,31 +14,44 @@
|
|
|
14
14
|
* a stored message.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { classifyBlock, NON_STRING_TEXT_PLACEHOLDER } from './references.js';
|
|
18
|
+
|
|
17
19
|
/**
|
|
18
20
|
* True when `content` holds nothing the model would see: no non-text block,
|
|
19
21
|
* and no text block whose text has a non-whitespace character. Mirrors the
|
|
20
22
|
* request sanitizer, which drops text that is not a string or trims to ''.
|
|
21
|
-
* Accepts MCPL wire blocks and converted membrane blocks alike.
|
|
23
|
+
* Accepts MCPL wire blocks and converted membrane blocks alike. A wire
|
|
24
|
+
* block is judged as the block the converter makes of it where that's text:
|
|
25
|
+
* MCP's embedded resource shows as the block its contents make
|
|
26
|
+
* (classifyBlock's `embedded`), so one whose contents are blank text holds
|
|
27
|
+
* nothing to see.
|
|
22
28
|
*/
|
|
23
29
|
export function isVisiblyEmptyContent(content: readonly unknown[] | null | undefined): boolean {
|
|
24
30
|
if (!Array.isArray(content)) return true;
|
|
25
31
|
for (const block of content) {
|
|
26
32
|
if (!block || typeof block !== 'object') continue;
|
|
27
33
|
const b = block as { type?: unknown; text?: unknown };
|
|
34
|
+
const shown = b.type === 'resource' ? embeddedText(b) : undefined;
|
|
35
|
+
if (shown !== undefined) {
|
|
36
|
+
if (shown.trim() !== '') return false;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
28
39
|
if (b.type !== 'text') return false;
|
|
29
40
|
if (typeof b.text === 'string' && b.text.trim() !== '') return false;
|
|
30
41
|
}
|
|
31
42
|
return true;
|
|
32
43
|
}
|
|
33
44
|
|
|
34
|
-
/**
|
|
35
|
-
*
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
/** The text an embedded resource's contents make, or undefined for any other
|
|
46
|
+
* block, an embedded resource shown as anything but text among them. */
|
|
47
|
+
function embeddedText(block: unknown): string | undefined {
|
|
48
|
+
const c = classifyBlock(block);
|
|
49
|
+
return c.kind === 'embedded' && c.contents.kind === 'block' && c.contents.block.type === 'text' ? c.contents.block.text : undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// What the converter shows for a text block whose text isn't a string,
|
|
53
|
+
// beside the converters' other markers in references.ts.
|
|
54
|
+
export { NON_STRING_TEXT_PLACEHOLDER };
|
|
42
55
|
|
|
43
56
|
/** A wire text block whose text isn't a string, which the converter shows as
|
|
44
57
|
* NON_STRING_TEXT_PLACEHOLDER. */
|