wave-agent-sdk 1.1.5 → 1.3.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/dist/agent.d.ts +128 -28
- package/dist/agent.js +201 -49
- package/dist/builtin/index.js +2 -0
- package/dist/builtin/plugins.js +11 -20
- package/dist/builtin/skills/settings.js +7 -20
- package/dist/builtin/skills/wave-daemon.d.ts +1 -0
- package/dist/builtin/skills/wave-daemon.js +194 -0
- package/dist/constants/images.d.ts +26 -0
- package/dist/constants/images.js +26 -0
- package/dist/constants/index.d.ts +16 -0
- package/dist/constants/index.js +16 -0
- package/dist/constants/memory.d.ts +26 -0
- package/dist/constants/memory.js +34 -0
- package/dist/constants/messages.d.ts +11 -0
- package/dist/constants/messages.js +11 -0
- package/dist/constants/plugins.d.ts +8 -0
- package/dist/constants/plugins.js +8 -0
- package/dist/constants/tools.d.ts +1 -0
- package/dist/constants/tools.js +1 -0
- package/dist/core/plugin.d.ts +54 -10
- package/dist/core/plugin.js +137 -23
- package/dist/core/session.d.ts +1 -1
- package/dist/core/session.js +1 -1
- package/dist/exec/catalog.d.ts +140 -0
- package/dist/exec/catalog.js +470 -0
- package/dist/exec/catalogAnnouncement.d.ts +89 -0
- package/dist/exec/catalogAnnouncement.js +293 -0
- package/dist/exec/constants.d.ts +51 -0
- package/dist/exec/constants.js +51 -0
- package/dist/exec/execRuntime.d.ts +55 -0
- package/dist/exec/execRuntime.js +217 -0
- package/dist/exec/workerSource.d.ts +28 -0
- package/dist/exec/workerSource.js +299 -0
- package/dist/host/index.d.ts +23 -0
- package/dist/host/index.js +23 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +8 -1
- package/dist/managers/MemoryRuleManager.d.ts +6 -0
- package/dist/managers/MemoryRuleManager.js +12 -0
- package/dist/managers/aiManager.d.ts +35 -25
- package/dist/managers/aiManager.js +204 -202
- package/dist/managers/backgroundTaskManager.js +14 -0
- package/dist/managers/bashModeManager.d.ts +33 -0
- package/dist/managers/bashModeManager.js +110 -0
- package/dist/managers/hookManager.d.ts +18 -0
- package/dist/managers/hookManager.js +37 -3
- package/dist/managers/liveConfigManager.d.ts +33 -0
- package/dist/managers/liveConfigManager.js +106 -11
- package/dist/managers/lspManager.d.ts +9 -0
- package/dist/managers/lspManager.js +47 -18
- package/dist/managers/mcpManager.d.ts +68 -10
- package/dist/managers/mcpManager.js +265 -15
- package/dist/managers/messageManager.d.ts +60 -18
- package/dist/managers/messageManager.js +170 -81
- package/dist/managers/permissionManager.d.ts +69 -0
- package/dist/managers/permissionManager.js +221 -78
- package/dist/managers/planManager.d.ts +9 -0
- package/dist/managers/planManager.js +19 -1
- package/dist/managers/pluginManager.d.ts +46 -2
- package/dist/managers/pluginManager.js +117 -11
- package/dist/managers/pluginScopeManager.d.ts +15 -2
- package/dist/managers/pluginScopeManager.js +20 -1
- package/dist/managers/skillManager.d.ts +50 -0
- package/dist/managers/skillManager.js +166 -12
- package/dist/managers/slashCommandManager.d.ts +10 -0
- package/dist/managers/slashCommandManager.js +44 -31
- package/dist/managers/subagentManager.d.ts +15 -0
- package/dist/managers/subagentManager.js +81 -9
- package/dist/managers/toolManager.d.ts +29 -3
- package/dist/managers/toolManager.js +87 -13
- package/dist/managers/workflowManager.js +6 -0
- package/dist/prompts/autoMemory.d.ts +9 -0
- package/dist/prompts/autoMemory.js +30 -31
- package/dist/prompts/autoMemoryExtraction.d.ts +4 -0
- package/dist/prompts/autoMemoryExtraction.js +8 -111
- package/dist/prompts/index.d.ts +0 -1
- package/dist/prompts/index.js +0 -4
- package/dist/prompts/memoryTypes.d.ts +63 -0
- package/dist/prompts/memoryTypes.js +191 -0
- package/dist/services/GitService.d.ts +7 -0
- package/dist/services/GitService.js +23 -0
- package/dist/services/MarketplaceService.d.ts +101 -17
- package/dist/services/MarketplaceService.js +323 -102
- package/dist/services/artifactContent.d.ts +84 -0
- package/dist/services/artifactContent.js +204 -0
- package/dist/services/artifactSession.d.ts +6 -0
- package/dist/services/artifactSession.js +17 -0
- package/dist/services/autoMemoryService.js +5 -13
- package/dist/services/configurationService.d.ts +92 -9
- package/dist/services/configurationService.js +246 -64
- package/dist/services/contentSummarizer.d.ts +15 -0
- package/dist/services/contentSummarizer.js +45 -0
- package/dist/services/execAvailability.d.ts +9 -0
- package/dist/services/execAvailability.js +32 -0
- package/dist/services/fileWatcher.js +61 -6
- package/dist/services/initializationService.js +21 -17
- package/dist/services/interactionService.d.ts +9 -1
- package/dist/services/interactionService.js +28 -8
- package/dist/services/jsonlHandler.d.ts +98 -0
- package/dist/services/jsonlHandler.js +250 -12
- package/dist/services/memory.d.ts +17 -1
- package/dist/services/memory.js +44 -7
- package/dist/services/officialMarketplaceMirror.d.ts +85 -0
- package/dist/services/officialMarketplaceMirror.js +290 -0
- package/dist/services/pluginLoader.d.ts +12 -4
- package/dist/services/pluginLoader.js +38 -7
- package/dist/services/remoteSettingsService.js +20 -6
- package/dist/services/session.d.ts +74 -0
- package/dist/services/session.js +174 -16
- package/dist/services/sessionEntries.d.ts +2 -0
- package/dist/services/sessionEntries.js +20 -0
- package/dist/services/worktreeHooks.js +6 -1
- package/dist/stdio/index.d.ts +12 -0
- package/dist/stdio/index.js +12 -0
- package/dist/stdio/notificationRouter.d.ts +38 -0
- package/dist/stdio/notificationRouter.js +97 -0
- package/dist/stdio/rpcClient.d.ts +18 -0
- package/dist/stdio/rpcClient.js +10 -0
- package/dist/stdio/stdioAgent.d.ts +229 -0
- package/dist/stdio/stdioAgent.js +360 -0
- package/dist/tools/artifactTool.js +406 -273
- package/dist/tools/bashTool.js +10 -6
- package/dist/tools/editTool.js +6 -3
- package/dist/tools/execTool.d.ts +2 -0
- package/dist/tools/execTool.js +165 -0
- package/dist/tools/exitPlanMode.js +10 -2
- package/dist/tools/grepTool.js +7 -1
- package/dist/tools/readTool.js +30 -2
- package/dist/tools/types.d.ts +34 -8
- package/dist/tools/webFetchTool.js +15 -166
- package/dist/tools/workflowTool.js +40 -8
- package/dist/tools/writeTool.js +6 -3
- package/dist/types/agent.d.ts +24 -1
- package/dist/types/commands.d.ts +7 -0
- package/dist/types/configuration.d.ts +45 -2
- package/dist/types/hooks.d.ts +1 -0
- package/dist/types/hooks.js +19 -0
- package/dist/types/marketplace.d.ts +40 -2
- package/dist/types/mcp.d.ts +42 -0
- package/dist/types/messaging.d.ts +1 -8
- package/dist/types/permissions.d.ts +22 -0
- package/dist/types/permissions.js +17 -0
- package/dist/types/plugins.d.ts +26 -2
- package/dist/types/skills.d.ts +26 -0
- package/dist/utils/bashParser.d.ts +17 -0
- package/dist/utils/bashParser.js +72 -0
- package/dist/utils/bashStructure/bashLexer.d.ts +96 -0
- package/dist/utils/bashStructure/bashLexer.js +676 -0
- package/dist/utils/bashStructure/bashParser.d.ts +144 -0
- package/dist/utils/bashStructure/bashParser.js +606 -0
- package/dist/utils/bashStructure/bashSemantics.d.ts +70 -0
- package/dist/utils/bashStructure/bashSemantics.js +477 -0
- package/dist/utils/bashStructure/index.d.ts +26 -0
- package/dist/utils/bashStructure/index.js +27 -0
- package/dist/utils/bashStructure/types.d.ts +62 -0
- package/dist/utils/bashStructure/types.js +47 -0
- package/dist/utils/constants.d.ts +10 -0
- package/dist/utils/constants.js +10 -0
- package/dist/utils/containerSetup.js +48 -6
- package/dist/utils/convertMessagesForAPI.d.ts +7 -1
- package/dist/utils/convertMessagesForAPI.js +64 -14
- package/dist/utils/fileChangeReminder.d.ts +20 -0
- package/dist/utils/fileChangeReminder.js +153 -0
- package/dist/utils/fileSearch.js +4 -3
- package/dist/utils/fileUtils.d.ts +44 -0
- package/dist/utils/fileUtils.js +118 -0
- package/dist/utils/frontmatterYaml.d.ts +33 -0
- package/dist/utils/frontmatterYaml.js +192 -0
- package/dist/utils/imageBudget.d.ts +85 -0
- package/dist/utils/imageBudget.js +109 -0
- package/dist/utils/imageDimensions.d.ts +83 -0
- package/dist/utils/imageDimensions.js +232 -0
- package/dist/utils/imageProcessor.d.ts +66 -0
- package/dist/utils/imageProcessor.js +84 -0
- package/dist/utils/imageRewrite.d.ts +29 -0
- package/dist/utils/imageRewrite.js +251 -0
- package/dist/utils/markdownParser.d.ts +5 -1
- package/dist/utils/markdownParser.js +9 -51
- package/dist/utils/mcpInstructions.d.ts +61 -0
- package/dist/utils/mcpInstructions.js +126 -0
- package/dist/utils/mcpUtils.d.ts +7 -0
- package/dist/utils/mcpUtils.js +11 -2
- package/dist/utils/memoryAge.d.ts +32 -0
- package/dist/utils/memoryAge.js +47 -0
- package/dist/utils/memoryEntrypoint.d.ts +20 -0
- package/dist/utils/memoryEntrypoint.js +49 -0
- package/dist/utils/memoryIndex.d.ts +30 -0
- package/dist/utils/memoryIndex.js +76 -0
- package/dist/utils/messageOperations.d.ts +6 -20
- package/dist/utils/messageOperations.js +40 -91
- package/dist/utils/nestedMemory.d.ts +22 -0
- package/dist/utils/nestedMemory.js +61 -0
- package/dist/utils/npmTarball.d.ts +19 -0
- package/dist/utils/npmTarball.js +92 -0
- package/dist/utils/pluginSource.d.ts +37 -0
- package/dist/utils/pluginSource.js +73 -0
- package/dist/utils/ripgrep.d.ts +18 -4
- package/dist/utils/ripgrep.js +56 -4
- package/dist/utils/runtimeDeps.d.ts +35 -0
- package/dist/utils/runtimeDeps.js +426 -0
- package/dist/utils/skillParser.js +22 -52
- package/dist/utils/subagentParser.js +48 -45
- package/dist/utils/tokenCalculation.js +0 -8
- package/dist/utils/userSettings.d.ts +90 -0
- package/dist/utils/userSettings.js +291 -0
- package/dist/utils/worktreeUtils.d.ts +2 -1
- package/dist/utils/worktreeUtils.js +64 -34
- package/package.json +12 -4
- package/dist/managers/bangManager.d.ts +0 -26
- package/dist/managers/bangManager.js +0 -78
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import { EXEC_RESERVED_NAMESPACE } from "./constants.js";
|
|
2
|
+
/**
|
|
3
|
+
* Recursion ceiling for signature rendering. Object, array and union recursion all
|
|
4
|
+
* increment depth, so this bounds every path: a pathological or structurally cyclic
|
|
5
|
+
* schema degrades to `unknown` instead of overflowing the stack. Rendering must
|
|
6
|
+
* never throw.
|
|
7
|
+
*
|
|
8
|
+
* This is the *only* silent reduction left in the renderer — there is deliberately
|
|
9
|
+
* no cap on properties per level or on enum variants. A wide schema or a long enum
|
|
10
|
+
* is decision-relevant text, and truncating it is how the model ends up guessing
|
|
11
|
+
* which parameter or which variant to use. Depth 8 and `unknown` are opencode's
|
|
12
|
+
* values (`tool-schema.ts`, `MAX_RENDER_DEPTH`).
|
|
13
|
+
*/
|
|
14
|
+
const MAX_SIGNATURE_DEPTH = 8;
|
|
15
|
+
/** Cap a rendered *tool* description at one line of this length. */
|
|
16
|
+
const MAX_DESCRIPTION_CHARS = 120;
|
|
17
|
+
/** A property name that can be written bare in TypeScript. */
|
|
18
|
+
const IDENTIFIER_SEGMENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
19
|
+
/**
|
|
20
|
+
* Budget accounting for the catalog, in estimated tokens.
|
|
21
|
+
*
|
|
22
|
+
* Applied to a whole catalog entry, which is a multi-line block: its newlines
|
|
23
|
+
* and indentation are part of `text.length`, so `chars / 4` already covers them.
|
|
24
|
+
*
|
|
25
|
+
* A plain `chars / 4`, same basis as opencode's `catalogBudget`. Deliberately
|
|
26
|
+
* not the CJK-aware `utils/tokenEstimate`: MCP tool descriptions are
|
|
27
|
+
* overwhelmingly English, so telling CJK from Latin text would not move the
|
|
28
|
+
* result in practice.
|
|
29
|
+
*/
|
|
30
|
+
const estimateCatalogTokens = (text) => Math.round(text.length / 4);
|
|
31
|
+
/**
|
|
32
|
+
* Every MCP tool the current agent may call, in catalog form.
|
|
33
|
+
*
|
|
34
|
+
* Derived from `getMcpToolsConfig()` — the exact source that produces the flat
|
|
35
|
+
* `tools[]` declarations when the pool is not collapsed, and the one whose
|
|
36
|
+
* connected/reconnecting filtering already keeps a transient disconnect from
|
|
37
|
+
* churning the tool list. So "the catalog equals what would have been declared"
|
|
38
|
+
* holds structurally, not by parallel construction.
|
|
39
|
+
*/
|
|
40
|
+
export function buildExecPool(mcpManager, permissionManager) {
|
|
41
|
+
const outputSchemas = mcpManager.getMcpToolOutputSchemas();
|
|
42
|
+
const entries = [];
|
|
43
|
+
for (const tool of mcpManager.getMcpToolsConfig()) {
|
|
44
|
+
if (permissionManager?.isToolDenied(tool.function.name))
|
|
45
|
+
continue;
|
|
46
|
+
entries.push({
|
|
47
|
+
name: tool.function.name,
|
|
48
|
+
description: tool.function.description,
|
|
49
|
+
inputSchema: tool.function.parameters,
|
|
50
|
+
outputSchema: outputSchemas.get(tool.function.name),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return entries;
|
|
54
|
+
}
|
|
55
|
+
/** Render an object key, quoting names that are not valid identifiers. */
|
|
56
|
+
function renderKey(name) {
|
|
57
|
+
return IDENTIFIER_SEGMENT.test(name) ? name : JSON.stringify(name);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The property path the sandbox exposes a tool under. Non-identifier names use
|
|
61
|
+
* bracket form because `tools.mcp__my-srv__x` would parse as a subtraction.
|
|
62
|
+
*/
|
|
63
|
+
function toolExpression(name) {
|
|
64
|
+
return IDENTIFIER_SEGMENT.test(name)
|
|
65
|
+
? `tools.${name}`
|
|
66
|
+
: `tools[${JSON.stringify(name)}]`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Trim a *tool* description to its first line and cap its length.
|
|
70
|
+
*
|
|
71
|
+
* Only tool-level prose is compressed here: it is padding that the model does not
|
|
72
|
+
* need to act, and the full text stays reachable through search. Field descriptions
|
|
73
|
+
* are never clamped (see `jsdoc`) — those carry the decision guidance.
|
|
74
|
+
*/
|
|
75
|
+
function clampDescription(text) {
|
|
76
|
+
const first = text.split("\n")[0].trim();
|
|
77
|
+
return first.length > MAX_DESCRIPTION_CHARS
|
|
78
|
+
? `${first.slice(0, MAX_DESCRIPTION_CHARS - 3)}...`
|
|
79
|
+
: first;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* JSDoc tags for a field. Only keywords that survive `cleanSchema` are
|
|
83
|
+
* supported; anything else (`minimum`, `pattern`, `title`, ...) is dropped.
|
|
84
|
+
*/
|
|
85
|
+
function docTags(schema) {
|
|
86
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema))
|
|
87
|
+
return [];
|
|
88
|
+
const typed = schema;
|
|
89
|
+
const tags = [];
|
|
90
|
+
if (typed.deprecated === true)
|
|
91
|
+
tags.push("@deprecated");
|
|
92
|
+
if (typed.default !== undefined) {
|
|
93
|
+
const rendered = JSON.stringify(typed.default);
|
|
94
|
+
if (rendered !== undefined)
|
|
95
|
+
tags.push(`@default ${rendered}`);
|
|
96
|
+
}
|
|
97
|
+
if (typeof typed.format === "string")
|
|
98
|
+
tags.push(`@format ${typed.format}`);
|
|
99
|
+
if (typeof typed.minItems === "number") {
|
|
100
|
+
tags.push(`@minItems ${typed.minItems}`);
|
|
101
|
+
}
|
|
102
|
+
if (typeof typed.maxItems === "number") {
|
|
103
|
+
tags.push(`@maxItems ${typed.maxItems}`);
|
|
104
|
+
}
|
|
105
|
+
return tags;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The JSDoc block rendered above one field, indented to `pad`. Emits nothing
|
|
109
|
+
* when the field has neither a description nor a tag, so plain fields stay
|
|
110
|
+
* unadorned.
|
|
111
|
+
*
|
|
112
|
+
* The description is kept verbatim — multi-line included, no width cap. A field
|
|
113
|
+
* description says what to put in the field ("which of these variants, and why"),
|
|
114
|
+
* so cutting it at a fixed column cuts the guidance and keeps the preamble. Only
|
|
115
|
+
* the tool's own description is clamped (see `clampDescription`); when a model
|
|
116
|
+
* needs that one in full it searches the pool by name.
|
|
117
|
+
*/
|
|
118
|
+
function jsdoc(schema, pad) {
|
|
119
|
+
const description = schema && typeof schema === "object" && !Array.isArray(schema)
|
|
120
|
+
? schema.description
|
|
121
|
+
: undefined;
|
|
122
|
+
const raw = [
|
|
123
|
+
...(typeof description === "string" ? description.split("\n") : []),
|
|
124
|
+
...docTags(schema),
|
|
125
|
+
]
|
|
126
|
+
// A `*/` inside third-party text would close the comment early. Split/join
|
|
127
|
+
// rather than `replaceAll`: these packages compile against lib ES2020.
|
|
128
|
+
.map((line) => line.split("*/").join("* /").replace(/\s+$/, ""));
|
|
129
|
+
while (raw.length > 0 && raw[0].trim() === "")
|
|
130
|
+
raw.shift();
|
|
131
|
+
while (raw.length > 0 && raw[raw.length - 1].trim() === "")
|
|
132
|
+
raw.pop();
|
|
133
|
+
if (raw.length === 0)
|
|
134
|
+
return "";
|
|
135
|
+
if (raw.length === 1)
|
|
136
|
+
return `${pad}/** ${raw[0]} */\n`;
|
|
137
|
+
const body = raw
|
|
138
|
+
.map((line) => `${pad} *${line === "" ? "" : ` ${line}`}`)
|
|
139
|
+
.join("\n");
|
|
140
|
+
return `${pad}/**\n${body}\n${pad} */\n`;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Render a JSON Schema as TypeScript: a multi-line block whose fields each carry
|
|
144
|
+
* their own JSDoc. Only recursion depth is capped; properties and enum variants
|
|
145
|
+
* are rendered in full.
|
|
146
|
+
*/
|
|
147
|
+
function renderType(schema, depth) {
|
|
148
|
+
if (depth > MAX_SIGNATURE_DEPTH)
|
|
149
|
+
return "unknown";
|
|
150
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
|
|
151
|
+
return "unknown";
|
|
152
|
+
}
|
|
153
|
+
const typed = schema;
|
|
154
|
+
if (Array.isArray(typed.enum)) {
|
|
155
|
+
return typed.enum
|
|
156
|
+
.map((value) => JSON.stringify(value) ?? "null")
|
|
157
|
+
.join(" | ");
|
|
158
|
+
}
|
|
159
|
+
const union = Array.isArray(typed.anyOf)
|
|
160
|
+
? typed.anyOf
|
|
161
|
+
: Array.isArray(typed.oneOf)
|
|
162
|
+
? typed.oneOf
|
|
163
|
+
: undefined;
|
|
164
|
+
if (union) {
|
|
165
|
+
// No cap on branches, for the reason there is none on properties or enum
|
|
166
|
+
// variants: a union is a list of choices, and showing the first few is how
|
|
167
|
+
// the model ends up using a branch that does not exist. opencode's renderer,
|
|
168
|
+
// whose depth ceiling this one borrows, renders every member too.
|
|
169
|
+
return union.map((variant) => renderType(variant, depth + 1)).join(" | ");
|
|
170
|
+
}
|
|
171
|
+
if (Array.isArray(typed.type)) {
|
|
172
|
+
const names = typed.type.filter((value) => typeof value === "string");
|
|
173
|
+
if (names.length > 0)
|
|
174
|
+
return names.join(" | ");
|
|
175
|
+
return "unknown";
|
|
176
|
+
}
|
|
177
|
+
if (typed.type === "array" || typed.items) {
|
|
178
|
+
return `Array<${renderType(typed.items, depth + 1)}>`;
|
|
179
|
+
}
|
|
180
|
+
if (typed.type === "object" || typed.properties) {
|
|
181
|
+
const properties = (typed.properties ?? {});
|
|
182
|
+
const required = new Set(Array.isArray(typed.required) ? typed.required : []);
|
|
183
|
+
const keys = Object.keys(properties);
|
|
184
|
+
if (keys.length === 0)
|
|
185
|
+
return "{}";
|
|
186
|
+
const pad = " ".repeat(depth + 1);
|
|
187
|
+
const close = " ".repeat(depth);
|
|
188
|
+
const lines = keys.map((key) => `${jsdoc(properties[key], pad)}${pad}${renderKey(key)}${required.has(key) ? "" : "?"}: ${renderType(properties[key], depth + 1)},`);
|
|
189
|
+
return `{\n${lines.join("\n")}\n${close}}`;
|
|
190
|
+
}
|
|
191
|
+
return typeof typed.type === "string" ? typed.type : "unknown";
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* The callable signature for one tool: parameters and return type. The catalog and
|
|
195
|
+
* search results share it, so the model can copy either one verbatim.
|
|
196
|
+
*
|
|
197
|
+
* The return type comes from the schema the server declared for its output, and a
|
|
198
|
+
* tool that declared none still gets `Promise<unknown>` rather than no return type
|
|
199
|
+
* at all: the sandbox resolves every call to the tool's output (its structured
|
|
200
|
+
* content, else its text, else `null`), and a signature ending at the parameters
|
|
201
|
+
* would read as "calls this, get nothing". Rendering it from the same rule the
|
|
202
|
+
* runtime applies is what keeps the catalog from teaching a shape the host will
|
|
203
|
+
* not deliver.
|
|
204
|
+
*/
|
|
205
|
+
export function renderToolSignature(entry) {
|
|
206
|
+
return `${toolExpression(entry.name)}(${renderType(entry.inputSchema, 0)}): Promise<${renderType(entry.outputSchema, 0)}>`;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* The path the sandbox exposes search under. Built from the reserved namespace
|
|
210
|
+
* (the sandbox builds it the same way), so prose and runtime cannot drift.
|
|
211
|
+
*/
|
|
212
|
+
const SEARCH_EXPRESSION = `tools[${JSON.stringify(EXEC_RESERVED_NAMESPACE)}].search`;
|
|
213
|
+
/**
|
|
214
|
+
* Input schema of the sandbox's `search` entry point.
|
|
215
|
+
*
|
|
216
|
+
* Load-bearing: the call form in the tool description, the call form in error
|
|
217
|
+
* messages and the validation `resolveSearchQuery` runs all come from this object.
|
|
218
|
+
* The description asked for the object form while the host only accepted a
|
|
219
|
+
* positional string, and nothing in the code tied the two together — one object
|
|
220
|
+
* makes that class of drift impossible rather than unlikely.
|
|
221
|
+
*/
|
|
222
|
+
const SEARCH_INPUT_SCHEMA = {
|
|
223
|
+
type: "object",
|
|
224
|
+
properties: {
|
|
225
|
+
query: {
|
|
226
|
+
type: "string",
|
|
227
|
+
description: "Substring matched against tool names and descriptions, case-insensitively. Omit it (or pass an empty string) to list the entire pool.",
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
232
|
+
* Output schema of the sandbox's `search` entry point.
|
|
233
|
+
*
|
|
234
|
+
* The same role `SEARCH_INPUT_SCHEMA` plays at the other end of the call: the
|
|
235
|
+
* return type in the rendered signature and the value the host actually hands back
|
|
236
|
+
* both come from this one object, so "what a hit looks like" cannot drift between
|
|
237
|
+
* the prose and the runtime. It also keeps the sandbox API uniform — every call
|
|
238
|
+
* resolves to its output, so nothing hands back a JSON *string* to parse.
|
|
239
|
+
*/
|
|
240
|
+
const SEARCH_OUTPUT_SCHEMA = {
|
|
241
|
+
type: "array",
|
|
242
|
+
items: {
|
|
243
|
+
type: "object",
|
|
244
|
+
properties: {
|
|
245
|
+
name: { type: "string" },
|
|
246
|
+
description: { type: "string" },
|
|
247
|
+
signature: { type: "string" },
|
|
248
|
+
},
|
|
249
|
+
required: ["name", "signature"],
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
const SEARCH_INPUT_KEYS = Object.keys(SEARCH_INPUT_SCHEMA.properties);
|
|
253
|
+
/** A type-shaped placeholder for a field, used by the one-line call form. */
|
|
254
|
+
function placeholderFor(schema) {
|
|
255
|
+
const type = schema && typeof schema === "object" && !Array.isArray(schema)
|
|
256
|
+
? schema.type
|
|
257
|
+
: undefined;
|
|
258
|
+
switch (type) {
|
|
259
|
+
case "string":
|
|
260
|
+
return '"..."';
|
|
261
|
+
case "number":
|
|
262
|
+
case "integer":
|
|
263
|
+
return "0";
|
|
264
|
+
case "boolean":
|
|
265
|
+
return "false";
|
|
266
|
+
case "array":
|
|
267
|
+
return "[]";
|
|
268
|
+
case "object":
|
|
269
|
+
return "{}";
|
|
270
|
+
default:
|
|
271
|
+
return "...";
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* The callable signature of search, rendered by the very function that renders
|
|
276
|
+
* every catalog entry, so its shape cannot drift from what the host accepts or
|
|
277
|
+
* returns. Multi-line, for the sandbox API blurb where there is room to show the
|
|
278
|
+
* field docs.
|
|
279
|
+
*/
|
|
280
|
+
export function renderSearchSignature() {
|
|
281
|
+
return `${SEARCH_EXPRESSION}(${renderType(SEARCH_INPUT_SCHEMA, 0)}): Promise<${renderType(SEARCH_OUTPUT_SCHEMA, 0)}>`;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* The one-line call form of search: the path plus a placeholder per field, all
|
|
285
|
+
* derived from `SEARCH_INPUT_SCHEMA`. For prose that cannot afford the multi-line
|
|
286
|
+
* block — the catalog's `PARTIAL` notice and error messages, which must still name
|
|
287
|
+
* a shape the host accepts.
|
|
288
|
+
*/
|
|
289
|
+
export function renderSearchCallForm() {
|
|
290
|
+
const properties = SEARCH_INPUT_SCHEMA.properties;
|
|
291
|
+
const fields = Object.keys(properties)
|
|
292
|
+
.map((key) => `${renderKey(key)}: ${placeholderFor(properties[key])}`)
|
|
293
|
+
.join(", ");
|
|
294
|
+
return `${SEARCH_EXPRESSION}({ ${fields} })`;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Validate a search call's arguments against `SEARCH_INPUT_SCHEMA` and return the
|
|
298
|
+
* query to match on.
|
|
299
|
+
*
|
|
300
|
+
* The sandbox only guarantees a plain object (see `callHost`); it knows nothing
|
|
301
|
+
* about this schema, so a call naming a field the schema does not have must fail
|
|
302
|
+
* loudly here. Treating `{ q: "..." }` as an empty query would answer "here is the
|
|
303
|
+
* whole pool" and dress a typo up as a successful search.
|
|
304
|
+
*/
|
|
305
|
+
export function resolveSearchQuery(args) {
|
|
306
|
+
const unexpected = Object.keys(args).find((key) => !SEARCH_INPUT_KEYS.includes(key));
|
|
307
|
+
if (unexpected !== undefined) {
|
|
308
|
+
throw new Error(`search() does not take "${unexpected}". Expected ${renderSearchCallForm()}`);
|
|
309
|
+
}
|
|
310
|
+
const query = args.query;
|
|
311
|
+
if (query === undefined)
|
|
312
|
+
return "";
|
|
313
|
+
if (typeof query !== "string") {
|
|
314
|
+
throw new Error(`search() expects "query" to be a string, got ${typeof query}. Expected ${renderSearchCallForm()}`);
|
|
315
|
+
}
|
|
316
|
+
return query.trim().toLowerCase();
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* A single catalog entry: its signature (possibly several lines) followed by the
|
|
320
|
+
* first line of its description.
|
|
321
|
+
*/
|
|
322
|
+
export function renderCatalogEntry(entry) {
|
|
323
|
+
const signature = renderToolSignature(entry);
|
|
324
|
+
const description = clampDescription(entry.description ?? "");
|
|
325
|
+
return description ? `${signature} // ${description}` : signature;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* The one-line index of a server: `- mcp__github (40 tools, 13 shown)`. The tail
|
|
329
|
+
* is dropped when the server is fully shown, and reads `none shown` when it got
|
|
330
|
+
* no seat at all.
|
|
331
|
+
*
|
|
332
|
+
* The prefix is the flattened name's, not the routing key alone, so the line is a
|
|
333
|
+
* substring of every tool name it covers (and of what search matches on). For a
|
|
334
|
+
* server whose name itself contains `__` the key is only the first segment — the
|
|
335
|
+
* same pre-existing ambiguity the grouping key has (see `execNamespace`).
|
|
336
|
+
*
|
|
337
|
+
* Exported for the announcement's per-namespace delta lines: the same renderer for
|
|
338
|
+
* the same counts, so a summary line and a delta line cannot drift apart.
|
|
339
|
+
*/
|
|
340
|
+
export function summarizeNamespace(name, count, shownCount) {
|
|
341
|
+
const label = `${count} tool${count === 1 ? "" : "s"}`;
|
|
342
|
+
const detail = shownCount === count
|
|
343
|
+
? ""
|
|
344
|
+
: shownCount === 0
|
|
345
|
+
? ", none shown"
|
|
346
|
+
: `, ${shownCount} shown`;
|
|
347
|
+
return `- mcp__${name} (${label}${detail})`;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* The server a flat MCP tool name routes to. Mirrors the split `executeMcpTool`
|
|
351
|
+
* does on the way back (`parts[1]`), so a group key always agrees with where a
|
|
352
|
+
* call would actually go — including its behaviour for server names that
|
|
353
|
+
* themselves contain `__`.
|
|
354
|
+
*/
|
|
355
|
+
function execNamespace(name) {
|
|
356
|
+
return name.split("__")[1] ?? name;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Cheapest rendered block first, ties broken by tool name.
|
|
360
|
+
*
|
|
361
|
+
* The rotation takes each server's next unshown entry every round, so this order
|
|
362
|
+
* *is* the seat order: cheapest-first means one budget buys the most entries, and
|
|
363
|
+
* the tie-break keeps the result a pure function of the entries themselves — the
|
|
364
|
+
* order `tools/list` happened to return is not a reason to seat one tool first.
|
|
365
|
+
* opencode ranks its listings the same way (`rankListings`: cost, then path).
|
|
366
|
+
*/
|
|
367
|
+
function orderByCost(group) {
|
|
368
|
+
return [...group].sort((left, right) => {
|
|
369
|
+
const delta = estimateCatalogTokens(renderCatalogEntry(left)) -
|
|
370
|
+
estimateCatalogTokens(renderCatalogEntry(right));
|
|
371
|
+
if (delta !== 0)
|
|
372
|
+
return delta;
|
|
373
|
+
return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
/** Groups in order of first appearance; within a group, cheapest block first. */
|
|
377
|
+
function groupByNamespace(entries) {
|
|
378
|
+
const groups = new Map();
|
|
379
|
+
for (const entry of entries) {
|
|
380
|
+
const namespace = execNamespace(entry.name);
|
|
381
|
+
const group = groups.get(namespace);
|
|
382
|
+
if (group) {
|
|
383
|
+
group.push(entry);
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
groups.set(namespace, [entry]);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return [...groups.values()].map(orderByCost);
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Render the catalog within an estimated-token budget.
|
|
393
|
+
*
|
|
394
|
+
* Selection round-robins across servers: one entry per server per round, so
|
|
395
|
+
* every server gets a seat before any server gets its second. A plain first-N
|
|
396
|
+
* cut would drop whole servers that happen to be connected later, and the model
|
|
397
|
+
* reads a missing server as "those tools do not exist" — the failure mode this
|
|
398
|
+
* whole feature has to avoid.
|
|
399
|
+
*
|
|
400
|
+
* An entry is a whole multi-line block and is budgeted and placed atomically: a
|
|
401
|
+
* server either gets the block this round or sits the round out, never half a
|
|
402
|
+
* signature.
|
|
403
|
+
*
|
|
404
|
+
* A truncated catalog additionally prints one summary line per server (see
|
|
405
|
+
* `summarizeNamespace`), before that server's blocks and outside the budget.
|
|
406
|
+
*
|
|
407
|
+
* The truncation notice deliberately carries no budget number: the budget is a
|
|
408
|
+
* tuning knob, and rendering it would make an unchanged tool pool produce
|
|
409
|
+
* different model-visible text after a config change.
|
|
410
|
+
*/
|
|
411
|
+
export function renderCatalog(entries, budgetTokens) {
|
|
412
|
+
const groups = groupByNamespace(entries);
|
|
413
|
+
const picked = groups.map(() => []);
|
|
414
|
+
let used = 0;
|
|
415
|
+
let shown = 0;
|
|
416
|
+
let active = groups.map((_, index) => index);
|
|
417
|
+
while (active.length > 0) {
|
|
418
|
+
const stillActive = [];
|
|
419
|
+
for (const index of active) {
|
|
420
|
+
const group = groups[index];
|
|
421
|
+
const entry = group[picked[index].length];
|
|
422
|
+
if (entry === undefined)
|
|
423
|
+
continue;
|
|
424
|
+
const block = renderCatalogEntry(entry);
|
|
425
|
+
const cost = estimateCatalogTokens(block) + 1;
|
|
426
|
+
// The very first entry is always shown even if it alone is over budget:
|
|
427
|
+
// an empty catalog would be worse than an over-budget one.
|
|
428
|
+
if (shown > 0 && used + cost > budgetTokens)
|
|
429
|
+
continue;
|
|
430
|
+
picked[index].push(block);
|
|
431
|
+
used += cost;
|
|
432
|
+
shown += 1;
|
|
433
|
+
if (picked[index].length < group.length)
|
|
434
|
+
stillActive.push(index);
|
|
435
|
+
}
|
|
436
|
+
active = stillActive;
|
|
437
|
+
}
|
|
438
|
+
const truncated = shown < entries.length;
|
|
439
|
+
// Rotation alone cannot promise any server a seat — a group whose block does
|
|
440
|
+
// not fit the remaining budget sits the round out for good — so a truncated
|
|
441
|
+
// catalog also names every server, one line each, outside the budget. Without
|
|
442
|
+
// that line a server nobody picked is invisible, and "not in the catalog" is
|
|
443
|
+
// read as "that capability does not exist". An untruncated catalog skips them:
|
|
444
|
+
// the entries are already the index.
|
|
445
|
+
const lines = [];
|
|
446
|
+
for (let index = 0; index < groups.length; index += 1) {
|
|
447
|
+
if (truncated) {
|
|
448
|
+
const group = groups[index];
|
|
449
|
+
lines.push(summarizeNamespace(execNamespace(group[0].name), group.length, picked[index].length));
|
|
450
|
+
}
|
|
451
|
+
lines.push(...picked[index]);
|
|
452
|
+
}
|
|
453
|
+
if (truncated) {
|
|
454
|
+
// Counts only: the call form that reaches the rest is taught once, in the
|
|
455
|
+
// announcement's search section, which exists in exactly this state. Writing it
|
|
456
|
+
// here as well would give the same call two spellings to drift between.
|
|
457
|
+
lines.push(`PARTIAL — ${shown} of ${entries.length} tools shown.`);
|
|
458
|
+
}
|
|
459
|
+
return {
|
|
460
|
+
text: lines.join("\n"),
|
|
461
|
+
shown,
|
|
462
|
+
total: entries.length,
|
|
463
|
+
truncated,
|
|
464
|
+
namespaces: groups.map((group, index) => ({
|
|
465
|
+
name: execNamespace(group[0].name),
|
|
466
|
+
count: group.length,
|
|
467
|
+
shown: picked[index].length,
|
|
468
|
+
})),
|
|
469
|
+
};
|
|
470
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MCP catalog, announced in the conversation instead of the `Exec` tool
|
|
3
|
+
* description. opencode only made this move on its v2 line (v1 shipped the catalog
|
|
4
|
+
* inside `execute`'s description, which is what this repo did until now).
|
|
5
|
+
*
|
|
6
|
+
* Why it cannot stay in the description: `tools[]` sits inside the cached prefix,
|
|
7
|
+
* and the catalog is a function of the pool. Every server that connects, drops, or
|
|
8
|
+
* changes its `tools/list` would rewrite the declaration and take the whole cached
|
|
9
|
+
* prefix with it. The description is a pool-independent constant now; the catalog is
|
|
10
|
+
* appended at the tail, where an addition invalidates nothing that already went out.
|
|
11
|
+
*
|
|
12
|
+
* State lives in the history, not in the process (same rule as
|
|
13
|
+
* `utils/mcpInstructions.ts`): each announcement leads with a marker line carrying
|
|
14
|
+
* the hash of a full catalog body. Three consequences, all deliberate:
|
|
15
|
+
* - Once the marker is gone (compaction, rewind), the next turn announces the full
|
|
16
|
+
* catalog again. A duplicate is better than a loss.
|
|
17
|
+
* - A delta is only emitted while the full catalog it is relative to is still in
|
|
18
|
+
* the history. The marker carries that baseline's hash (`bh`) so the check stays
|
|
19
|
+
* local: a delta is relative, and without its basis it says nothing.
|
|
20
|
+
* - The hash covers rendered text, so editing the renderer re-announces the catalog
|
|
21
|
+
* once for existing sessions. That is what buys the invariant "same hash ⇒ the
|
|
22
|
+
* same bytes the model read". It is also why a full catalog carries no "this
|
|
23
|
+
* supersedes the previous one" preamble: such a line would depend on history, and
|
|
24
|
+
* a history-dependent body can never compare equal to a freshly rendered one —
|
|
25
|
+
* the session would re-announce on every single turn.
|
|
26
|
+
*/
|
|
27
|
+
import type { Message } from "../types/messaging.js";
|
|
28
|
+
import type { RenderedCatalog } from "./catalog.js";
|
|
29
|
+
/** Marks an announcement so the history scan can find it. */
|
|
30
|
+
export declare const EXEC_CATALOG_MARKER_PREFIX = "<!-- exec-catalog ";
|
|
31
|
+
export declare const EXEC_CATALOG_MARKER_SUFFIX = " -->";
|
|
32
|
+
/** Which body a marker leads. */
|
|
33
|
+
type ExecCatalogAnnouncementKind = "full" | "delta" | "removed";
|
|
34
|
+
/**
|
|
35
|
+
* A marker's payload: what a later turn needs to *compare* against, never the body
|
|
36
|
+
* itself. The body is the message the marker leads.
|
|
37
|
+
*/
|
|
38
|
+
export interface ExecCatalogMarker {
|
|
39
|
+
kind: ExecCatalogAnnouncementKind;
|
|
40
|
+
/**
|
|
41
|
+
* Hash of the full catalog body as of this announcement — for a delta too, where
|
|
42
|
+
* it is the hash of what a full catalog would say right now. That is what lets the
|
|
43
|
+
* next turn (whose pool has not moved on since) recognise the delta as current and
|
|
44
|
+
* stay silent.
|
|
45
|
+
*/
|
|
46
|
+
hash?: string;
|
|
47
|
+
/** 1 when the catalog was truncated at that moment. */
|
|
48
|
+
truncated?: number;
|
|
49
|
+
/** Namespace → how many tools it held. The only material a delta can be built from. */
|
|
50
|
+
namespaces?: Record<string, number>;
|
|
51
|
+
/** Delta only: the hash of the full catalog this delta is relative to. */
|
|
52
|
+
base?: string;
|
|
53
|
+
}
|
|
54
|
+
/** What the history says: its last announcement, and every full catalog hash in it. */
|
|
55
|
+
export interface ExecCatalogState {
|
|
56
|
+
last: ExecCatalogMarker | null;
|
|
57
|
+
fullHashes: Set<string>;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A full catalog body: the header, the rendered entries, and — only when the budget
|
|
61
|
+
* truncated them — how to reach the rest.
|
|
62
|
+
*
|
|
63
|
+
* The search section is the single place the call form is taught (the entry point
|
|
64
|
+
* stays registered either way, it is merely not advertised). Making it conditional
|
|
65
|
+
* is what keeps the two statements from drifting: while the catalog is complete,
|
|
66
|
+
* nothing anywhere claims a search is needed.
|
|
67
|
+
*/
|
|
68
|
+
export declare function renderFullCatalogNote(catalog: RenderedCatalog): string;
|
|
69
|
+
/**
|
|
70
|
+
* Read the announcement state out of the history.
|
|
71
|
+
*
|
|
72
|
+
* Only this module's own messages are read: a marker quoted anywhere else — a reply
|
|
73
|
+
* explaining the mechanism, a hook echoing one — is prose *about* a marker, not a
|
|
74
|
+
* statement about the pool. Counting it would invent an announcement, and the
|
|
75
|
+
* invented state then misleads every later turn. A marker that cannot be parsed is
|
|
76
|
+
* skipped for the same reason (worst case: one duplicate announcement).
|
|
77
|
+
*/
|
|
78
|
+
export declare function collectExecCatalogState(messages: Message[]): ExecCatalogState;
|
|
79
|
+
/**
|
|
80
|
+
* The next announcement for this turn, or null when there is nothing to say.
|
|
81
|
+
*
|
|
82
|
+
* The decision table, in order: a channel that was never open says nothing; a closed
|
|
83
|
+
* channel is announced once; a session that has announced nothing (or whose last word
|
|
84
|
+
* was the closing notice) gets the full catalog; an unchanged hash says nothing; a
|
|
85
|
+
* changed pool with its basis still present may go out as a namespace-level delta;
|
|
86
|
+
* anything else is the full catalog.
|
|
87
|
+
*/
|
|
88
|
+
export declare function buildExecCatalogAnnouncement(state: ExecCatalogState, current: RenderedCatalog | undefined): string | null;
|
|
89
|
+
export {};
|