@webmcp-auto-ui/agent 2.5.35 → 2.5.37
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/package.json +1 -1
- package/src/autoui-server.ts +38 -0
- package/src/discovery-cache.ts +16 -8
- package/src/index.ts +2 -2
- package/src/loop.ts +55 -26
- package/src/tool-layers.ts +2 -3
package/package.json
CHANGED
package/src/autoui-server.ts
CHANGED
|
@@ -54,6 +54,40 @@ When the user wants to browse, search, or pick a recipe — for example "show me
|
|
|
54
54
|
Call widget_display({name: "recipe-browser", params: {recipes: [...], layout: "list"}}). The widget emits a bubbling 'widget:interact' CustomEvent with detail={action:"pick", payload: recipe} when the user clicks Pick.
|
|
55
55
|
`;
|
|
56
56
|
|
|
57
|
+
// Inline recipe for tool-browser (real vanilla widget)
|
|
58
|
+
const toolBrowserRecipe = `---
|
|
59
|
+
widget: tool-browser
|
|
60
|
+
description: Interactive tool browser with search/filters/preview. Use when the user wants to browse, search, or pick a tool from connected servers.
|
|
61
|
+
group: rich
|
|
62
|
+
schema:
|
|
63
|
+
type: object
|
|
64
|
+
required:
|
|
65
|
+
- tools
|
|
66
|
+
properties:
|
|
67
|
+
tools:
|
|
68
|
+
type: array
|
|
69
|
+
description: List of BrowsableTool objects (name, description, server, inputSchema, ...).
|
|
70
|
+
items:
|
|
71
|
+
type: object
|
|
72
|
+
filters:
|
|
73
|
+
type: object
|
|
74
|
+
description: Initial filters
|
|
75
|
+
properties:
|
|
76
|
+
q:
|
|
77
|
+
type: string
|
|
78
|
+
layout:
|
|
79
|
+
type: string
|
|
80
|
+
enum: [list, grid]
|
|
81
|
+
description: Default layout (default list)
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## When to use
|
|
85
|
+
When the user wants to browse, search, or pick a tool — for example "show me the available tools".
|
|
86
|
+
|
|
87
|
+
## How to use
|
|
88
|
+
Call widget_display({name: "tool-browser", params: {tools: [...]}}). The widget emits a bubbling 'widget:interact' CustomEvent on user actions.
|
|
89
|
+
`;
|
|
90
|
+
|
|
57
91
|
// ---------------------------------------------------------------------------
|
|
58
92
|
// Inline recipes (frontmatter + body)
|
|
59
93
|
// ---------------------------------------------------------------------------
|
|
@@ -930,6 +964,7 @@ Call widget_display({name: "chat-input", params: {placeholder: "Your reply..."}}
|
|
|
930
964
|
const _NOTEBOOK_RECIPE_SOURCES: string[] = [
|
|
931
965
|
notebookRecipe as string,
|
|
932
966
|
recipeBrowserRecipe,
|
|
967
|
+
toolBrowserRecipe,
|
|
933
968
|
];
|
|
934
969
|
export const NATIVE_WIDGET_NAMES = [...RECIPES, ..._NOTEBOOK_RECIPE_SOURCES].map(r => {
|
|
935
970
|
const match = r.match(/widget:\s*(\S+)/);
|
|
@@ -955,6 +990,9 @@ autoui.registerWidget(notebookRecipe as string, renderNotebook as any);
|
|
|
955
990
|
// Recipe browser — resolved by WidgetRenderer as <auto-recipe-browser> custom element
|
|
956
991
|
autoui.registerWidget(recipeBrowserRecipe, undefined);
|
|
957
992
|
|
|
993
|
+
// Tool browser — resolved by WidgetRenderer as <auto-tool-browser> custom element
|
|
994
|
+
autoui.registerWidget(toolBrowserRecipe, undefined);
|
|
995
|
+
|
|
958
996
|
// Register flow recipes (multi-step procedures) from the global recipe registry
|
|
959
997
|
// that declare this server (autoui) in their frontmatter.
|
|
960
998
|
for (const [key, rawMd] of Object.entries(RAW_RECIPES)) {
|
package/src/discovery-cache.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type { ProviderTool } from './types.js';
|
|
8
8
|
import type { PipelineTrace } from './pipeline-trace.js';
|
|
9
|
+
import { sanitizeServerName } from './tool-layers.js';
|
|
9
10
|
|
|
10
11
|
/** Tool names that are resolved locally from cache — hidden from user-facing browsers. */
|
|
11
12
|
export const DISCOVERY_TOOL_NAMES = new Set(['list_recipes', 'search_recipes', 'get_recipe', 'list_tools', 'search_tools']);
|
|
@@ -30,14 +31,21 @@ export interface ServerCache {
|
|
|
30
31
|
export class DiscoveryCache {
|
|
31
32
|
private servers = new Map<string, ServerCache>();
|
|
32
33
|
|
|
34
|
+
/** Normalize a server prefix so register/lookup share the same key shape
|
|
35
|
+
* regardless of casing, spaces, or other artifacts. Mirrors the
|
|
36
|
+
* sanitization applied to tool name prefixes by tool-layers. */
|
|
37
|
+
private norm(serverPrefix: string): string {
|
|
38
|
+
return sanitizeServerName(serverPrefix);
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
/** Register a server's recipes and tools */
|
|
34
42
|
register(serverPrefix: string, data: ServerCache): void {
|
|
35
|
-
this.servers.set(serverPrefix, data);
|
|
43
|
+
this.servers.set(this.norm(serverPrefix), data);
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
/** Check if we have cached data for a server prefix */
|
|
39
47
|
has(serverPrefix: string): boolean {
|
|
40
|
-
return this.servers.has(serverPrefix);
|
|
48
|
+
return this.servers.has(this.norm(serverPrefix));
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
/** Clear all cached data */
|
|
@@ -45,7 +53,7 @@ export class DiscoveryCache {
|
|
|
45
53
|
this.servers.clear();
|
|
46
54
|
}
|
|
47
55
|
|
|
48
|
-
/** All registered server prefixes */
|
|
56
|
+
/** All registered server prefixes (normalized) */
|
|
49
57
|
serverPrefixes(): string[] {
|
|
50
58
|
return [...this.servers.keys()];
|
|
51
59
|
}
|
|
@@ -70,22 +78,22 @@ export class DiscoveryCache {
|
|
|
70
78
|
|
|
71
79
|
/** Recipe count for a specific server */
|
|
72
80
|
recipeCount(serverPrefix: string): number {
|
|
73
|
-
return this.servers.get(serverPrefix)?.recipes.length ?? 0;
|
|
81
|
+
return this.servers.get(this.norm(serverPrefix))?.recipes.length ?? 0;
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
/** Get the cached recipes for a specific server prefix. */
|
|
77
85
|
recipesFor(serverPrefix: string): CachedRecipe[] {
|
|
78
|
-
return this.servers.get(serverPrefix)?.recipes ?? [];
|
|
86
|
+
return this.servers.get(this.norm(serverPrefix))?.recipes ?? [];
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
/** Tool count for a specific server */
|
|
82
90
|
toolCount(serverPrefix: string): number {
|
|
83
|
-
return this.servers.get(serverPrefix)?.tools.length ?? 0;
|
|
91
|
+
return this.servers.get(this.norm(serverPrefix))?.tools.length ?? 0;
|
|
84
92
|
}
|
|
85
93
|
|
|
86
94
|
/** Tool count excluding discovery tools (hidden from user-facing browsers) */
|
|
87
95
|
browsableToolCount(serverPrefix: string): number {
|
|
88
|
-
const tools = this.servers.get(serverPrefix)?.tools ?? [];
|
|
96
|
+
const tools = this.servers.get(this.norm(serverPrefix))?.tools ?? [];
|
|
89
97
|
return tools.filter(t => !DISCOVERY_TOOL_NAMES.has(t.name)).length;
|
|
90
98
|
}
|
|
91
99
|
|
|
@@ -103,7 +111,7 @@ export class DiscoveryCache {
|
|
|
103
111
|
params: Record<string, unknown>,
|
|
104
112
|
trace?: PipelineTrace,
|
|
105
113
|
): string | null {
|
|
106
|
-
const cache = this.servers.get(serverPrefix);
|
|
114
|
+
const cache = this.servers.get(this.norm(serverPrefix));
|
|
107
115
|
if (!cache) return null;
|
|
108
116
|
|
|
109
117
|
switch (realToolName) {
|
package/src/index.ts
CHANGED
|
@@ -32,7 +32,7 @@ export { GemmaProvider } from './providers/gemma.js';
|
|
|
32
32
|
export type { GemmaProviderOptions, GemmaStatus } from './providers/gemma.js';
|
|
33
33
|
|
|
34
34
|
// Agent loop
|
|
35
|
-
export { runAgentLoop, toProviderTools, fromMcpTools, trimConversationHistory } from './loop.js';
|
|
35
|
+
export { runAgentLoop, toProviderTools, fromMcpTools, trimConversationHistory, pruneOrphanToolResults } from './loop.js';
|
|
36
36
|
export { buildSystemPrompt, buildSystemPromptWithAliases } from './prompts/index.js';
|
|
37
37
|
export type { SystemPromptResult } from './prompts/index.js';
|
|
38
38
|
export type { AgentLoopOptions } from './loop.js';
|
|
@@ -41,7 +41,7 @@ export type { AgentLoopOptions } from './loop.js';
|
|
|
41
41
|
export { autoui, NATIVE_WIDGET_NAMES } from './autoui-server.js';
|
|
42
42
|
|
|
43
43
|
// Tool layers
|
|
44
|
-
export { buildToolsFromLayers, buildDiscoveryTools, buildDiscoveryToolsWithAliases, activateServerTools, resolveCanonicalTools, toolAliasMap, flattenPathMaps, buildDiscoveryCache } from './tool-layers.js';
|
|
44
|
+
export { buildToolsFromLayers, buildDiscoveryTools, buildDiscoveryToolsWithAliases, activateServerTools, resolveCanonicalTools, toolAliasMap, flattenPathMaps, buildDiscoveryCache, sanitizeServerName } from './tool-layers.js';
|
|
45
45
|
export type { ToolLayer, McpLayer, WebMcpLayer, DiscoveryToolsResult, SchemaTransformOptions, BuildToolsResult, ProviderKind } from './tool-layers.js';
|
|
46
46
|
|
|
47
47
|
// Discovery cache
|
package/src/loop.ts
CHANGED
|
@@ -275,6 +275,10 @@ export async function runAgentLoop(
|
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
+
// Defensive: strip any orphan tool_result blocks before sending — strict
|
|
279
|
+
// providers (Anthropic) reject these with a 400.
|
|
280
|
+
pruneOrphanToolResults(messages);
|
|
281
|
+
|
|
278
282
|
callbacks.onLLMRequest?.(messages, iterationTools);
|
|
279
283
|
const t0 = performance.now();
|
|
280
284
|
let streamingText = '';
|
|
@@ -456,10 +460,18 @@ export async function runAgentLoop(
|
|
|
456
460
|
if (!client) {
|
|
457
461
|
result = `Error: no MCP client available for tool ${name}`;
|
|
458
462
|
} else {
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
+
const mcpLayer = (options.layers ?? []).find(
|
|
464
|
+
l => sanitizeServerName(l.serverName) === serverName && l.protocol === 'mcp'
|
|
465
|
+
) as { serverUrl?: string } | undefined;
|
|
466
|
+
const serverUrl = mcpLayer?.serverUrl;
|
|
467
|
+
if (!serverUrl) {
|
|
468
|
+
result = `Error: no serverUrl resolved for tool "${name}" (server "${serverName}"). Pass serverUrl on the matching ToolLayer.`;
|
|
469
|
+
} else {
|
|
470
|
+
const mcpResult = await (client as unknown as { callToolOn: (u: string, n: string, a: unknown) => Promise<{ content?: { type: string; text?: string }[] }> }).callToolOn(serverUrl, realToolName, toolInput);
|
|
471
|
+
const textContent = mcpResult.content?.find((c: { type: string }) => c.type === 'text') as { text?: string } | undefined;
|
|
472
|
+
const rawResult = textContent?.text ?? JSON.stringify(mcpResult);
|
|
473
|
+
result = truncateResults ? truncateResult(rawResult, maxResultLength) : rawResult;
|
|
474
|
+
}
|
|
463
475
|
}
|
|
464
476
|
} else if (protocol === 'webmcp') {
|
|
465
477
|
// Intercept recall BEFORE hitting executeTool — use the local resultBuffer directly
|
|
@@ -539,6 +551,15 @@ export async function runAgentLoop(
|
|
|
539
551
|
const c = messages[i].content;
|
|
540
552
|
if (Array.isArray(c) && c.length === 0) messages.splice(i, 1);
|
|
541
553
|
}
|
|
554
|
+
// Also drop pending tool_results in the current iteration whose
|
|
555
|
+
// tool_use was just stripped from the assistant turn — otherwise
|
|
556
|
+
// they become orphans when pushed at the end of the loop.
|
|
557
|
+
for (let j = toolResults.length - 1; j >= 0; j--) {
|
|
558
|
+
const b = toolResults[j];
|
|
559
|
+
if (b.type === 'tool_result' && strippedIds.has((b as { tool_use_id: string }).tool_use_id)) {
|
|
560
|
+
toolResults.splice(j, 1);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
542
563
|
}
|
|
543
564
|
hasRendered = false;
|
|
544
565
|
}
|
|
@@ -663,18 +684,39 @@ export function trimConversationHistory(history: ChatMessage[], maxTokens: numbe
|
|
|
663
684
|
|
|
664
685
|
// Remove orphaned tool_result blocks anywhere in history — strict providers
|
|
665
686
|
// (Anthropic, etc.) reject tool_result blocks whose tool_use_id does not
|
|
666
|
-
// correspond to an earlier assistant tool_use.
|
|
667
|
-
|
|
687
|
+
// correspond to an earlier assistant tool_use.
|
|
688
|
+
pruneOrphanToolResults(trimmed);
|
|
689
|
+
|
|
690
|
+
// Ensure the first non-system message is role=user (API requirement)
|
|
691
|
+
while (trimmed.length > 0) {
|
|
692
|
+
const firstNonSystem = trimmed.findIndex(m => m.role !== 'system');
|
|
693
|
+
if (firstNonSystem >= 0 && trimmed[firstNonSystem].role === 'assistant') {
|
|
694
|
+
trimmed.splice(firstNonSystem, 1);
|
|
695
|
+
} else {
|
|
696
|
+
break;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return trimmed;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Strip orphan tool_result blocks (whose tool_use_id has no matching tool_use
|
|
705
|
+
* in any preceding assistant message) and drop user messages that become
|
|
706
|
+
* empty as a result. Mutates `messages` in place.
|
|
707
|
+
*
|
|
708
|
+
* Strict providers (Anthropic API) reject orphans with a 400. This guard
|
|
709
|
+
* runs immediately before each provider call.
|
|
710
|
+
*/
|
|
711
|
+
export function pruneOrphanToolResults(messages: ChatMessage[]): void {
|
|
668
712
|
const validToolUseIds = new Set<string>();
|
|
669
|
-
for (let i = 0; i <
|
|
670
|
-
const msg =
|
|
671
|
-
// Collect tool_use ids from assistant messages seen so far
|
|
713
|
+
for (let i = 0; i < messages.length; i++) {
|
|
714
|
+
const msg = messages[i];
|
|
672
715
|
if (msg.role === 'assistant' && Array.isArray(msg.content)) {
|
|
673
716
|
for (const b of msg.content as any[]) {
|
|
674
717
|
if (b?.type === 'tool_use' && typeof b.id === 'string') validToolUseIds.add(b.id);
|
|
675
718
|
}
|
|
676
719
|
}
|
|
677
|
-
// Filter out orphan tool_result blocks in user messages
|
|
678
720
|
if (msg.role === 'user' && Array.isArray(msg.content)) {
|
|
679
721
|
msg.content = (msg.content as any[]).filter(b => {
|
|
680
722
|
if (b?.type !== 'tool_result') return true;
|
|
@@ -682,21 +724,8 @@ export function trimConversationHistory(history: ChatMessage[], maxTokens: numbe
|
|
|
682
724
|
}) as any;
|
|
683
725
|
}
|
|
684
726
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
if (Array.isArray(c) && c.length === 0) trimmed.splice(i, 1);
|
|
727
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
728
|
+
const c = messages[i].content;
|
|
729
|
+
if (Array.isArray(c) && c.length === 0) messages.splice(i, 1);
|
|
689
730
|
}
|
|
690
|
-
|
|
691
|
-
// Ensure the first non-system message is role=user (API requirement)
|
|
692
|
-
while (trimmed.length > 0) {
|
|
693
|
-
const firstNonSystem = trimmed.findIndex(m => m.role !== 'system');
|
|
694
|
-
if (firstNonSystem >= 0 && trimmed[firstNonSystem].role === 'assistant') {
|
|
695
|
-
trimmed.splice(firstNonSystem, 1);
|
|
696
|
-
} else {
|
|
697
|
-
break;
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
return trimmed;
|
|
702
731
|
}
|
package/src/tool-layers.ts
CHANGED
|
@@ -261,10 +261,9 @@ export function toProviderTools(tools: McpToolDef[], schemaOptions?: SchemaTrans
|
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
|
-
|
|
265
|
-
// Ensure root schema has additionalProperties for strict mode
|
|
264
|
+
let schemaObj = schema as Record<string, unknown>;
|
|
266
265
|
if (schemaObj.type === 'object' && !('additionalProperties' in schemaObj)) {
|
|
267
|
-
schemaObj
|
|
266
|
+
schemaObj = { ...schemaObj, additionalProperties: false };
|
|
268
267
|
}
|
|
269
268
|
return {
|
|
270
269
|
name: t.name,
|