@gt-fe/eap-runtime 0.0.13 → 0.0.14
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 +3 -1
- package/dist/api/handlers/invoke.handler.d.ts +1 -1
- package/dist/api/handlers/invoke.handler.js +3 -0
- package/dist/api/handlers/stream.handler.d.ts +1 -1
- package/dist/api/handlers/stream.handler.js +4 -1
- package/dist/api/routes/agent.routes.d.ts +2 -2
- package/dist/common/index.d.ts +1 -0
- package/dist/common/index.js +1 -0
- package/dist/common/logger.d.ts +16 -4
- package/dist/common/logger.js +82 -16
- package/dist/common/runtime-package-version.d.ts +2 -0
- package/dist/common/runtime-package-version.js +5 -0
- package/dist/common/types.d.ts +5 -5
- package/dist/common/types.js +2 -2
- package/dist/contracts/permission-checker.d.ts +1 -1
- package/dist/debug/llm-turn-log.d.ts +12 -0
- package/dist/debug/llm-turn-log.js +1 -0
- package/dist/governance/governance-client.d.ts +1 -1
- package/dist/governance/index.d.ts +1 -1
- package/dist/governance/permission-guard.d.ts +1 -1
- package/dist/graph/adapters/langgraph.adapter.d.ts +3 -1
- package/dist/graph/adapters/langgraph.adapter.js +67 -14
- package/dist/graph/adapters/subgraph-compiler.js +5 -2
- package/dist/graph/contracts/executable-graph.d.ts +19 -8
- package/dist/graph/contracts/executable-graph.js +1 -1
- package/dist/graph/nodes/llm-node.d.ts +3 -0
- package/dist/graph/nodes/llm-node.js +8 -0
- package/dist/graph/nodes/skill-bind-node.d.ts +3 -4
- package/dist/graph/nodes/skill-bind-node.js +1 -20
- package/dist/graph/nodes/tool-node.d.ts +2 -0
- package/dist/graph/nodes/tool-node.js +20 -14
- package/dist/index.d.ts +1 -1
- package/dist/memory/compression/index.d.ts +1 -1
- package/dist/memory/compression/index.js +1 -1
- package/dist/memory/compression/sliding-window.compression.js +7 -7
- package/dist/memory/index.d.ts +1 -1
- package/dist/memory/index.js +1 -1
- package/dist/memory/message-compression.service.d.ts +12 -1
- package/dist/memory/message-compression.service.js +29 -1
- package/dist/providers/http-llm-provider.d.ts +1 -0
- package/dist/providers/http-llm-provider.js +4 -1
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.js +1 -1
- package/dist/providers/local-llm-provider.d.ts +4 -1
- package/dist/providers/local-llm-provider.js +15 -3
- package/dist/providers/routing-llm-provider.js +13 -4
- package/dist/registry/agent-models.d.ts +1 -1
- package/dist/registry/agent-models.js +1 -1
- package/dist/registry/agent-registry-client.d.ts +1 -1
- package/dist/registry/deployment-snapshot.js +19 -17
- package/dist/registry/graph-loader.d.ts +11 -1
- package/dist/registry/graph-loader.js +115 -66
- package/dist/registry/preload-errors.d.ts +1 -1
- package/dist/registry/preload-errors.js +1 -1
- package/dist/registry/skill-io-guard.js +1 -1
- package/dist/registry/skill-tree.js +1 -1
- package/dist/runtime/agent-runtime.d.ts +3 -3
- package/dist/runtime/agent-runtime.js +14 -4
- package/dist/runtime/dev-runtime.d.ts +2 -0
- package/dist/runtime/dev-runtime.js +2 -0
- package/dist/runtime/types.d.ts +4 -4
- package/dist/server.js +1 -1
- package/dist/session/compression-middleware.d.ts +2 -1
- package/dist/session/compression-middleware.js +7 -4
- package/dist/sse/events/skill-events.d.ts +1 -1
- package/dist/sse/events/skill-events.js +1 -1
- package/dist/sse/events/stats-events.d.ts +9 -0
- package/dist/token/compression-budget-integration.d.ts +1 -1
- package/dist/token/compression-budget-integration.js +18 -10
- package/dist/token/context-window.d.ts +42 -0
- package/dist/token/context-window.js +73 -0
- package/dist/token/index.d.ts +1 -0
- package/dist/token/index.js +1 -0
- package/dist/tool/create-tool-executor.d.ts +1 -1
- package/dist/tool/dev-http-executor.d.ts +2 -1
- package/dist/tool/dev-http-executor.js +14 -2
- package/dist/tool/dev-mcp-executor.d.ts +11 -1
- package/dist/tool/dev-mcp-executor.js +27 -1
- package/dist/tool/dev-tool-executor.d.ts +2 -1
- package/dist/tool/dev-tool-executor.js +6 -0
- package/dist/tool/harness-command.js +1 -1
- package/dist/tool/harness-primitives.d.ts +7 -0
- package/dist/tool/harness-primitives.js +57 -6
- package/dist/tool/http-tool-executor.js +1 -1
- package/dist/tool/index.d.ts +2 -1
- package/dist/tool/index.js +1 -0
- package/dist/tool/local-mcp-client.d.ts +5 -2
- package/dist/tool/local-mcp-client.js +130 -3
- package/dist/tool/local-tool-executor.d.ts +3 -2
- package/dist/tool/local-tool-executor.js +16 -1
- package/dist/tool/mcp-list.d.ts +53 -0
- package/dist/tool/mcp-list.js +199 -0
- package/dist/tool/mcp-tool-executor.d.ts +6 -1
- package/dist/tool/mcp-tool-executor.js +53 -1
- package/dist/tool/skill-dir-infer.d.ts +17 -0
- package/dist/tool/skill-dir-infer.js +81 -0
- package/dist/tool/skill-script-refs.d.ts +1 -8
- package/dist/tool/skill-script-refs.js +32 -20
- package/dist/tool/system-prompt-builder.d.ts +35 -16
- package/dist/tool/system-prompt-builder.js +30 -58
- package/dist/tool/timeout.d.ts +2 -2
- package/dist/tool/timeout.js +6 -3
- package/dist/tool/tool-execution-router.d.ts +3 -2
- package/dist/tool/tool-execution-router.js +8 -1
- package/dist/tool/tool-executor.interface.d.ts +11 -0
- package/dist/tool/tool-orchestrator.d.ts +1 -1
- package/dist/tool/tool-orchestrator.js +1 -1
- package/dist/utils/format-error.d.ts +2 -2
- package/dist/utils/format-error.js +2 -2
- package/dist/utils/http-error.d.ts +2 -2
- package/dist/utils/http-error.js +2 -2
- package/package.json +6 -33
- package/dist/registry/mcp-stdio-cwd.d.ts +0 -5
- package/dist/registry/mcp-stdio-cwd.js +0 -13
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* 优先 ILocalToolInstallStore;若无安装记录但 request.execution 带
|
|
5
5
|
* 或 request.execution 带可解析的 code bundlePath+handler,则直接本机执行。
|
|
6
6
|
*/
|
|
7
|
-
import type { ILocalToolInstallStore } from '@
|
|
8
|
-
import type { IToolExecutor, ToolCallRequest, ToolCallResult } from './tool-executor.interface.js';
|
|
7
|
+
import type { ILocalToolInstallStore } from '@eap/contracts';
|
|
8
|
+
import type { IToolExecutor, McpListedTool, ToolCallRequest, ToolCallResult } from './tool-executor.interface.js';
|
|
9
9
|
import { type ILocalMcpClient } from './local-mcp-client.js';
|
|
10
10
|
export interface LocalToolExecutorOptions {
|
|
11
11
|
installStore: ILocalToolInstallStore;
|
|
@@ -30,6 +30,7 @@ export declare class LocalToolExecutor implements IToolExecutor {
|
|
|
30
30
|
private readonly mcpClient;
|
|
31
31
|
constructor(options: LocalToolExecutorOptions);
|
|
32
32
|
execute(request: ToolCallRequest): Promise<ToolCallResult>;
|
|
33
|
+
listTools(request: ToolCallRequest): Promise<McpListedTool[]>;
|
|
33
34
|
private executeCodeSpec;
|
|
34
35
|
private executeCode;
|
|
35
36
|
private executeMcp;
|
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
import { spawn } from 'node:child_process';
|
|
8
8
|
import { existsSync } from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
|
-
import { ErrorCodes, resolveReadyInstallByToolName, resolveMcpExecutionFromInstall, resolveCodeHandlerFromInstall, } from '@
|
|
10
|
+
import { ErrorCodes, resolveReadyInstallByToolName, resolveMcpExecutionFromInstall, resolveCodeHandlerFromInstall, } from '@eap/contracts';
|
|
11
11
|
import { isAbortError, toToolTimeoutMs, toolTimeoutErrorMessage } from './timeout.js';
|
|
12
12
|
import { fileArtifactFromToolOutput, fileBasesFromEnv } from './file-artifact.js';
|
|
13
13
|
import { LocalMcpClient } from './local-mcp-client.js';
|
|
14
|
+
import { normalizeMcpExecution } from './dev-mcp-executor.js';
|
|
14
15
|
/**
|
|
15
16
|
* 根据 execution.runtime 解析执行命令:
|
|
16
17
|
* - node(默认)→ process.execPath + [handlerPath]
|
|
@@ -171,6 +172,20 @@ export class LocalToolExecutor {
|
|
|
171
172
|
runtime: 'local',
|
|
172
173
|
};
|
|
173
174
|
}
|
|
175
|
+
async listTools(request) {
|
|
176
|
+
const fromRequest = normalizeMcpExecution(request.execution, request.toolName);
|
|
177
|
+
if (fromRequest) {
|
|
178
|
+
return this.mcpClient.list(fromRequest, { userId: request.authContext?.userId });
|
|
179
|
+
}
|
|
180
|
+
const install = resolveReadyInstallByToolName(this.options.installStore, request.toolName);
|
|
181
|
+
if (install && install.installStatus === 'ready' && (install.type ?? request.toolType) === 'mcp') {
|
|
182
|
+
const execution = resolveMcpExecutionFromInstall(install);
|
|
183
|
+
if (execution) {
|
|
184
|
+
return this.mcpClient.list(execution, { userId: request.authContext?.userId });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
throw new Error(`MCP tools/list: no mcp execution for "${request.toolName}"`);
|
|
188
|
+
}
|
|
174
189
|
async executeCodeSpec(request, spec) {
|
|
175
190
|
try {
|
|
176
191
|
const stdout = await runHandler(spec.handlerPath, request.arguments, spec.timeoutMs ?? this.options.timeoutMs ?? 30_000, { runtime: spec.runtime, args: spec.args });
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tools/list 解析、discover stub 判定、hydrate。
|
|
3
|
+
*
|
|
4
|
+
* type: mcp 的预装工具集不展开清单 tools[],先产出 discover stub;
|
|
5
|
+
* 有 ExecutionContext 后由同一执行器 tools/list,再写入 LLM tools。
|
|
6
|
+
*/
|
|
7
|
+
import type { LoadedToolDefinition, LoadedToolMap } from './loaded-tool-definition.js';
|
|
8
|
+
import type { AuthContext, IToolExecutor, McpListedTool } from './tool-executor.interface.js';
|
|
9
|
+
export declare function parseListedTools(result: unknown): McpListedTool[];
|
|
10
|
+
export declare function nextListCursor(result: unknown): string | undefined;
|
|
11
|
+
export interface McpDiscoverExecution {
|
|
12
|
+
protocol?: string;
|
|
13
|
+
discover?: boolean;
|
|
14
|
+
transport?: string;
|
|
15
|
+
server?: Record<string, unknown>;
|
|
16
|
+
timeout?: number;
|
|
17
|
+
bundlePath?: string;
|
|
18
|
+
enabledTools?: string[];
|
|
19
|
+
autoApprove?: string[];
|
|
20
|
+
}
|
|
21
|
+
export declare function readMcpDiscoverExecution(raw: unknown): McpDiscoverExecution | null;
|
|
22
|
+
export declare function isMcpDiscoverStub(def: LoadedToolDefinition): boolean;
|
|
23
|
+
export declare function mcpEndpointKey(execution: unknown, fallback?: string): string;
|
|
24
|
+
export declare const MCP_LIST_TOOL_NAME = "tools/list";
|
|
25
|
+
/** 展示用:`tl-data-035/get_dataset_by_code`;无名工具集则只返回工具名。 */
|
|
26
|
+
export declare function formatToolsetToolName(toolset: string | undefined, toolName: string): string;
|
|
27
|
+
/** 从 hydrate 后的 toolId(`toolset:toolName`)取出工具集编码。 */
|
|
28
|
+
export declare function toolsetCodeFromLoaded(loaded: {
|
|
29
|
+
toolId?: string;
|
|
30
|
+
name?: string;
|
|
31
|
+
manifest?: {
|
|
32
|
+
id?: string;
|
|
33
|
+
};
|
|
34
|
+
} | undefined): string | undefined;
|
|
35
|
+
export interface HydrateMcpToolsAuth {
|
|
36
|
+
authContext: AuthContext;
|
|
37
|
+
correlationId?: string;
|
|
38
|
+
taskId?: string;
|
|
39
|
+
onListFailure?: (failure: {
|
|
40
|
+
toolset: string;
|
|
41
|
+
endpoint: string;
|
|
42
|
+
error: string;
|
|
43
|
+
}) => void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 将 MCP discover stub 替换为 tools/list 结果。
|
|
47
|
+
* 无 stub 时原样返回(同一引用)。按 endpoint+user+agent 在执行器内缓存 list。
|
|
48
|
+
* enabledTools 为可选白名单;autoApprove 的子工具 requireConfirmation=false。
|
|
49
|
+
* 单个 endpoint 的 list 失败只跳过该工具集;全部失败才抛错(错误含工具集名)。
|
|
50
|
+
*/
|
|
51
|
+
export declare function hydrateMcpTools(loadedTools: LoadedToolMap | undefined, executor: IToolExecutor | undefined, auth: HydrateMcpToolsAuth): Promise<LoadedToolMap | undefined>;
|
|
52
|
+
/** 测试与 HITL 续跑:从已 hydrate 的 map 判断是否还要 list */
|
|
53
|
+
export declare function needsMcpHydration(loadedTools: LoadedToolMap | undefined): boolean;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tools/list 解析、discover stub 判定、hydrate。
|
|
3
|
+
*
|
|
4
|
+
* type: mcp 的预装工具集不展开清单 tools[],先产出 discover stub;
|
|
5
|
+
* 有 ExecutionContext 后由同一执行器 tools/list,再写入 LLM tools。
|
|
6
|
+
*/
|
|
7
|
+
import { emitRuntimeLog } from '../common/logger.js';
|
|
8
|
+
export function parseListedTools(result) {
|
|
9
|
+
const tools = result?.tools;
|
|
10
|
+
if (!Array.isArray(tools))
|
|
11
|
+
return [];
|
|
12
|
+
const out = [];
|
|
13
|
+
for (const raw of tools) {
|
|
14
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
15
|
+
continue;
|
|
16
|
+
const t = raw;
|
|
17
|
+
if (typeof t.name !== 'string' || !t.name.trim())
|
|
18
|
+
continue;
|
|
19
|
+
const inputSchema = t.inputSchema && typeof t.inputSchema === 'object' && !Array.isArray(t.inputSchema)
|
|
20
|
+
? t.inputSchema
|
|
21
|
+
: undefined;
|
|
22
|
+
out.push({
|
|
23
|
+
name: t.name,
|
|
24
|
+
description: typeof t.description === 'string' ? t.description : undefined,
|
|
25
|
+
inputSchema,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
export function nextListCursor(result) {
|
|
31
|
+
const cursor = result?.nextCursor;
|
|
32
|
+
return typeof cursor === 'string' && cursor.trim() ? cursor : undefined;
|
|
33
|
+
}
|
|
34
|
+
export function readMcpDiscoverExecution(raw) {
|
|
35
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
36
|
+
return null;
|
|
37
|
+
const e = raw;
|
|
38
|
+
if (e.protocol !== 'mcp' || e.discover !== true)
|
|
39
|
+
return null;
|
|
40
|
+
return e;
|
|
41
|
+
}
|
|
42
|
+
export function isMcpDiscoverStub(def) {
|
|
43
|
+
return def.manifest.type === 'mcp' && readMcpDiscoverExecution(def.manifest.execution) != null;
|
|
44
|
+
}
|
|
45
|
+
export function mcpEndpointKey(execution, fallback = 'gateway') {
|
|
46
|
+
if (!execution || typeof execution !== 'object' || Array.isArray(execution))
|
|
47
|
+
return fallback;
|
|
48
|
+
const e = execution;
|
|
49
|
+
const server = e.server;
|
|
50
|
+
if (server && typeof server === 'object') {
|
|
51
|
+
if (typeof server.url === 'string' && server.url.trim()) {
|
|
52
|
+
return `http:${server.url.trim()}`;
|
|
53
|
+
}
|
|
54
|
+
if (typeof server.command === 'string' && server.command.trim()) {
|
|
55
|
+
const args = Array.isArray(server.args)
|
|
56
|
+
? server.args.map((a) => String(a)).join('\0')
|
|
57
|
+
: '';
|
|
58
|
+
return `stdio:${server.command.trim()}:${args}`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return fallback;
|
|
62
|
+
}
|
|
63
|
+
export const MCP_LIST_TOOL_NAME = 'tools/list';
|
|
64
|
+
/** 展示用:`tl-data-035/get_dataset_by_code`;无名工具集则只返回工具名。 */
|
|
65
|
+
export function formatToolsetToolName(toolset, toolName) {
|
|
66
|
+
const set = toolset?.trim() ?? '';
|
|
67
|
+
const name = toolName.trim() || 'tool';
|
|
68
|
+
if (!set || set === name)
|
|
69
|
+
return name;
|
|
70
|
+
if (name.startsWith(`${set}/`))
|
|
71
|
+
return name;
|
|
72
|
+
return `${set}/${name}`;
|
|
73
|
+
}
|
|
74
|
+
/** 从 hydrate 后的 toolId(`toolset:toolName`)取出工具集编码。 */
|
|
75
|
+
export function toolsetCodeFromLoaded(loaded) {
|
|
76
|
+
if (!loaded)
|
|
77
|
+
return undefined;
|
|
78
|
+
const id = loaded.toolId || loaded.manifest?.id || '';
|
|
79
|
+
const sep = id.indexOf(':');
|
|
80
|
+
if (sep > 0) {
|
|
81
|
+
const prefix = id.slice(0, sep);
|
|
82
|
+
if (prefix && prefix !== loaded.name)
|
|
83
|
+
return prefix;
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 将 MCP discover stub 替换为 tools/list 结果。
|
|
89
|
+
* 无 stub 时原样返回(同一引用)。按 endpoint+user+agent 在执行器内缓存 list。
|
|
90
|
+
* enabledTools 为可选白名单;autoApprove 的子工具 requireConfirmation=false。
|
|
91
|
+
* 单个 endpoint 的 list 失败只跳过该工具集;全部失败才抛错(错误含工具集名)。
|
|
92
|
+
*/
|
|
93
|
+
export async function hydrateMcpTools(loadedTools, executor, auth) {
|
|
94
|
+
if (!loadedTools)
|
|
95
|
+
return loadedTools;
|
|
96
|
+
const stubs = Object.values(loadedTools).filter(isMcpDiscoverStub);
|
|
97
|
+
if (stubs.length === 0)
|
|
98
|
+
return loadedTools;
|
|
99
|
+
if (typeof executor?.listTools !== 'function') {
|
|
100
|
+
throw new Error('MCP 工具集需要 tools/list,当前执行器不支持 listTools');
|
|
101
|
+
}
|
|
102
|
+
const result = { ...loadedTools };
|
|
103
|
+
for (const stub of stubs) {
|
|
104
|
+
delete result[stub.name];
|
|
105
|
+
}
|
|
106
|
+
const groups = new Map();
|
|
107
|
+
for (const stub of stubs) {
|
|
108
|
+
const key = mcpEndpointKey(stub.manifest.execution);
|
|
109
|
+
const list = groups.get(key);
|
|
110
|
+
if (list)
|
|
111
|
+
list.push(stub);
|
|
112
|
+
else
|
|
113
|
+
groups.set(key, [stub]);
|
|
114
|
+
}
|
|
115
|
+
const emitted = new Set();
|
|
116
|
+
const failures = [];
|
|
117
|
+
for (const [, group] of groups) {
|
|
118
|
+
const primary = group[0];
|
|
119
|
+
const endpoint = mcpEndpointKey(primary.manifest.execution, primary.name);
|
|
120
|
+
let listed;
|
|
121
|
+
try {
|
|
122
|
+
listed = await executor.listTools({
|
|
123
|
+
toolCallId: `mcp-list:${primary.toolId}`,
|
|
124
|
+
toolName: primary.name,
|
|
125
|
+
arguments: {},
|
|
126
|
+
authContext: auth.authContext,
|
|
127
|
+
correlationId: auth.correlationId,
|
|
128
|
+
taskId: auth.taskId,
|
|
129
|
+
toolType: 'mcp',
|
|
130
|
+
toolRuntime: primary.manifest.toolRuntime,
|
|
131
|
+
execution: primary.manifest.execution,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
136
|
+
const ref = formatToolsetToolName(primary.name, MCP_LIST_TOOL_NAME);
|
|
137
|
+
const message = `MCP ${ref} 失败 (${endpoint}): ${detail}`;
|
|
138
|
+
failures.push(message);
|
|
139
|
+
emitRuntimeLog('warn', 'tool', message, {
|
|
140
|
+
agentCode: auth.authContext.agentCode,
|
|
141
|
+
taskId: auth.taskId,
|
|
142
|
+
correlationId: auth.correlationId,
|
|
143
|
+
});
|
|
144
|
+
auth.onListFailure?.({ toolset: primary.name, endpoint, error: detail });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
for (const stub of group) {
|
|
148
|
+
const exec = readMcpDiscoverExecution(stub.manifest.execution);
|
|
149
|
+
const enabled = exec?.enabledTools;
|
|
150
|
+
const enabledSet = enabled && enabled.length > 0 ? new Set(enabled) : null;
|
|
151
|
+
const autoApprove = new Set(exec?.autoApprove ?? []);
|
|
152
|
+
const picked = enabledSet ? listed.filter((t) => enabledSet.has(t.name)) : listed;
|
|
153
|
+
for (const tool of picked) {
|
|
154
|
+
if (emitted.has(tool.name))
|
|
155
|
+
continue;
|
|
156
|
+
emitted.add(tool.name);
|
|
157
|
+
result[tool.name] = listedToolToDefinition(tool, stub, autoApprove.has(tool.name));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (emitted.size === 0 && failures.length > 0) {
|
|
162
|
+
throw new Error(failures.join('\n'));
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
function listedToolToDefinition(tool, stub, autoApprove) {
|
|
167
|
+
const exec = { ...(readMcpDiscoverExecution(stub.manifest.execution) ?? {}) };
|
|
168
|
+
delete exec.discover;
|
|
169
|
+
delete exec.enabledTools;
|
|
170
|
+
delete exec.autoApprove;
|
|
171
|
+
const rest = exec;
|
|
172
|
+
return {
|
|
173
|
+
toolId: `${stub.toolId}:${tool.name}`,
|
|
174
|
+
name: tool.name,
|
|
175
|
+
manifest: {
|
|
176
|
+
id: `${stub.manifest.id}:${tool.name}`,
|
|
177
|
+
name: tool.name,
|
|
178
|
+
version: stub.manifest.version,
|
|
179
|
+
type: 'mcp',
|
|
180
|
+
status: 'active',
|
|
181
|
+
description: tool.description,
|
|
182
|
+
toolRuntime: stub.manifest.toolRuntime,
|
|
183
|
+
dataScope: stub.manifest.dataScope,
|
|
184
|
+
requireConfirmation: autoApprove ? false : Boolean(stub.manifest.requireConfirmation),
|
|
185
|
+
execution: {
|
|
186
|
+
...rest,
|
|
187
|
+
protocol: 'mcp',
|
|
188
|
+
toolName: tool.name,
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
inputSchema: tool.inputSchema ?? { type: 'object', properties: {} },
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/** 测试与 HITL 续跑:从已 hydrate 的 map 判断是否还要 list */
|
|
195
|
+
export function needsMcpHydration(loadedTools) {
|
|
196
|
+
if (!loadedTools)
|
|
197
|
+
return false;
|
|
198
|
+
return Object.values(loadedTools).some(isMcpDiscoverStub);
|
|
199
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @see docs/contracts/tool-execution.md §2.0
|
|
5
5
|
*/
|
|
6
|
-
import type { IToolExecutor, ToolCallRequest, ToolCallResult } from './tool-executor.interface.js';
|
|
6
|
+
import type { IToolExecutor, McpListedTool, ToolCallRequest, ToolCallResult } from './tool-executor.interface.js';
|
|
7
7
|
export interface McpToolExecutorOptions {
|
|
8
8
|
baseUrl: string;
|
|
9
9
|
timeout?: number;
|
|
@@ -16,8 +16,13 @@ export declare class McpToolExecutor implements IToolExecutor {
|
|
|
16
16
|
private readonly mcpUrl;
|
|
17
17
|
private rpcId;
|
|
18
18
|
private initialized;
|
|
19
|
+
/** endpoint+user+agent → in-flight / completed tools/list */
|
|
20
|
+
private readonly listCache;
|
|
19
21
|
constructor(options: McpToolExecutorOptions);
|
|
20
22
|
execute(request: ToolCallRequest): Promise<ToolCallResult>;
|
|
23
|
+
listTools(request: ToolCallRequest): Promise<McpListedTool[]>;
|
|
24
|
+
private listAllTools;
|
|
25
|
+
private rpcResult;
|
|
21
26
|
private ensureInitialized;
|
|
22
27
|
private postRpc;
|
|
23
28
|
}
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @see docs/contracts/tool-execution.md §2.0
|
|
5
5
|
*/
|
|
6
|
-
import { ErrorCodes } from '@
|
|
6
|
+
import { ErrorCodes } from '@eap/contracts';
|
|
7
7
|
import { abortOrTimeoutMessage, isAbortError } from './timeout.js';
|
|
8
|
+
import { nextListCursor, parseListedTools } from './mcp-list.js';
|
|
8
9
|
function resolveAgentCode(request) {
|
|
9
10
|
return request.authContext.agentCode;
|
|
10
11
|
}
|
|
@@ -45,6 +46,8 @@ export class McpToolExecutor {
|
|
|
45
46
|
mcpUrl;
|
|
46
47
|
rpcId = 1;
|
|
47
48
|
initialized = false;
|
|
49
|
+
/** endpoint+user+agent → in-flight / completed tools/list */
|
|
50
|
+
listCache = new Map();
|
|
48
51
|
constructor(options) {
|
|
49
52
|
this.baseUrl = options.baseUrl.replace(/\/+$/, '');
|
|
50
53
|
this.timeout = options.timeout ?? 30_000;
|
|
@@ -125,6 +128,55 @@ export class McpToolExecutor {
|
|
|
125
128
|
};
|
|
126
129
|
}
|
|
127
130
|
}
|
|
131
|
+
async listTools(request) {
|
|
132
|
+
const key = `${this.mcpUrl}\0${request.authContext.userId}\0${request.authContext.agentCode}`;
|
|
133
|
+
const cached = this.listCache.get(key);
|
|
134
|
+
if (cached)
|
|
135
|
+
return cached;
|
|
136
|
+
const pending = this.listAllTools(request);
|
|
137
|
+
this.listCache.set(key, pending);
|
|
138
|
+
try {
|
|
139
|
+
return await pending;
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
this.listCache.delete(key);
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async listAllTools(request) {
|
|
147
|
+
await this.ensureInitialized(request);
|
|
148
|
+
const tools = [];
|
|
149
|
+
let cursor;
|
|
150
|
+
do {
|
|
151
|
+
const result = await this.rpcResult(request, 'tools/list', cursor ? { cursor } : {});
|
|
152
|
+
tools.push(...parseListedTools(result));
|
|
153
|
+
cursor = nextListCursor(result);
|
|
154
|
+
} while (cursor);
|
|
155
|
+
return tools;
|
|
156
|
+
}
|
|
157
|
+
async rpcResult(request, method, params) {
|
|
158
|
+
const agentCode = resolveAgentCode(request);
|
|
159
|
+
const correlationId = request.correlationId ?? request.toolCallId;
|
|
160
|
+
const id = this.rpcId++;
|
|
161
|
+
const response = await this.postRpc({ jsonrpc: '2.0', id, method, params }, request, agentCode, correlationId);
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
let body = null;
|
|
164
|
+
try {
|
|
165
|
+
body = (await response.json());
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
body = null;
|
|
169
|
+
}
|
|
170
|
+
const mapped = mapMcpError(response.status, body);
|
|
171
|
+
throw new Error(mapped.error ?? `MCP ${method} failed: HTTP ${response.status}`);
|
|
172
|
+
}
|
|
173
|
+
const body = (await response.json());
|
|
174
|
+
if (body.error) {
|
|
175
|
+
const mapped = mapMcpError(500, body);
|
|
176
|
+
throw new Error(mapped.error ?? `MCP ${method} failed`);
|
|
177
|
+
}
|
|
178
|
+
return body.result;
|
|
179
|
+
}
|
|
128
180
|
async ensureInitialized(request) {
|
|
129
181
|
if (this.initialized)
|
|
130
182
|
return;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 推断 harness 用的技能包根:命令里的绝对路径、本批/历史 read_file、bind、唯一技能。
|
|
3
|
+
* 不写 GraphState.activatedSkill。
|
|
4
|
+
*/
|
|
5
|
+
import { type SkillDirRef } from '../graph/adapters/skill-read.js';
|
|
6
|
+
import type { GraphMessage } from '../graph/contracts/executable-graph.js';
|
|
7
|
+
export declare function longestSkillDirInText(text: string, skillDirs: SkillDirRef[]): string | undefined;
|
|
8
|
+
export declare function inferHarnessSkillDir(opts: {
|
|
9
|
+
command?: string;
|
|
10
|
+
toolCalls?: Array<{
|
|
11
|
+
name?: string;
|
|
12
|
+
arguments?: Record<string, unknown>;
|
|
13
|
+
}>;
|
|
14
|
+
messages?: GraphMessage[];
|
|
15
|
+
skillDirs: SkillDirRef[];
|
|
16
|
+
activatedSkill?: string | null;
|
|
17
|
+
}): string | undefined;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 推断 harness 用的技能包根:命令里的绝对路径、本批/历史 read_file、bind、唯一技能。
|
|
3
|
+
* 不写 GraphState.activatedSkill。
|
|
4
|
+
*/
|
|
5
|
+
import { classifySkillRead } from '../graph/adapters/skill-read.js';
|
|
6
|
+
import { normalizeSkillDir } from './skill-script-refs.js';
|
|
7
|
+
export function longestSkillDirInText(text, skillDirs) {
|
|
8
|
+
const hay = text.replace(/\\/g, '/');
|
|
9
|
+
let best;
|
|
10
|
+
for (const dir of skillDirs) {
|
|
11
|
+
if (!dir.bundlePath)
|
|
12
|
+
continue;
|
|
13
|
+
const root = normalizeSkillDir(dir.bundlePath);
|
|
14
|
+
if (root && hay.includes(root) && (!best || root.length > best.len)) {
|
|
15
|
+
best = { bundlePath: dir.bundlePath, len: root.length };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return best?.bundlePath;
|
|
19
|
+
}
|
|
20
|
+
function bundleForSkillId(skillId, skillDirs) {
|
|
21
|
+
return skillDirs.find((dir) => dir.skillId === skillId)?.bundlePath;
|
|
22
|
+
}
|
|
23
|
+
function lastReadSkillDir(toolCalls, skillDirs) {
|
|
24
|
+
if (!toolCalls?.length)
|
|
25
|
+
return undefined;
|
|
26
|
+
let found;
|
|
27
|
+
for (const tc of toolCalls) {
|
|
28
|
+
const hit = classifySkillRead(tc.name ?? '', tc.arguments, skillDirs);
|
|
29
|
+
if (hit?.skillId) {
|
|
30
|
+
const bundle = bundleForSkillId(hit.skillId, skillDirs);
|
|
31
|
+
if (bundle)
|
|
32
|
+
found = bundle;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return found;
|
|
36
|
+
}
|
|
37
|
+
function inferFromMessages(messages, skillDirs) {
|
|
38
|
+
if (!messages?.length)
|
|
39
|
+
return undefined;
|
|
40
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
41
|
+
const msg = messages[i];
|
|
42
|
+
if (msg.role === 'assistant' && msg.toolCalls?.length) {
|
|
43
|
+
const found = lastReadSkillDir(msg.toolCalls.map((tc) => ({ name: tc.name, arguments: tc.arguments })), skillDirs);
|
|
44
|
+
if (found)
|
|
45
|
+
return found;
|
|
46
|
+
}
|
|
47
|
+
if (msg.role === 'tool' && typeof msg.content === 'string') {
|
|
48
|
+
const tagged = msg.content.match(/技能包根(只读)([^\s。]+)|EAP_SKILL_DIR=([^\s。]+)/);
|
|
49
|
+
const dir = tagged?.[1] ?? tagged?.[2];
|
|
50
|
+
if (dir) {
|
|
51
|
+
const norm = normalizeSkillDir(dir);
|
|
52
|
+
const hit = skillDirs.find((d) => d.bundlePath && normalizeSkillDir(d.bundlePath) === norm);
|
|
53
|
+
if (hit?.bundlePath)
|
|
54
|
+
return hit.bundlePath;
|
|
55
|
+
}
|
|
56
|
+
const fromBody = longestSkillDirInText(msg.content, skillDirs);
|
|
57
|
+
if (fromBody)
|
|
58
|
+
return fromBody;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
export function inferHarnessSkillDir(opts) {
|
|
64
|
+
const dirs = opts.skillDirs.filter((dir) => Boolean(dir.bundlePath));
|
|
65
|
+
if (opts.command) {
|
|
66
|
+
const fromCommand = longestSkillDirInText(opts.command, dirs);
|
|
67
|
+
if (fromCommand)
|
|
68
|
+
return fromCommand;
|
|
69
|
+
}
|
|
70
|
+
const fromBatch = lastReadSkillDir(opts.toolCalls, dirs);
|
|
71
|
+
if (fromBatch)
|
|
72
|
+
return fromBatch;
|
|
73
|
+
const fromHistory = inferFromMessages(opts.messages, dirs);
|
|
74
|
+
if (fromHistory)
|
|
75
|
+
return fromHistory;
|
|
76
|
+
if (dirs.length === 1)
|
|
77
|
+
return dirs[0].bundlePath;
|
|
78
|
+
if (opts.activatedSkill)
|
|
79
|
+
return bundleForSkillId(opts.activatedSkill, dirs);
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
@@ -6,15 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export declare const EAP_SKILL_DIR_ENV = "EAP_SKILL_DIR";
|
|
8
8
|
export interface ExpandSkillRefsOptions {
|
|
9
|
-
platform?: string;
|
|
10
9
|
exists?: (absPath: string) => boolean;
|
|
11
10
|
}
|
|
12
11
|
export declare function normalizeSkillDir(skillDir: string): string;
|
|
13
|
-
export declare function
|
|
14
|
-
/**
|
|
15
|
-
* 包内相对脚本路径 → 技能目录绝对路径。
|
|
16
|
-
* 带目录或 `./` 的(`bin/foo.py`、`./main.py`、`scripts/a.py`)一律展开。
|
|
17
|
-
* 根上的裸文件名(`main.py`)仅当包内确实有该文件才展开,以免改写「写到用户目录再跑」的例子。
|
|
18
|
-
*/
|
|
19
|
-
export declare function expandSkillScriptRefs(body: string, skillDir: string, platformOrOpts?: string | ExpandSkillRefsOptions): string;
|
|
12
|
+
export declare function expandSkillScriptRefs(body: string, skillDir: string, opts?: ExpandSkillRefsOptions): string;
|
|
20
13
|
export declare function isSkillEntryFile(filePath: string): boolean;
|
|
@@ -13,14 +13,6 @@ const RELATIVE_CODE_PATH = new RegExp(`(^|[^$\\w./\\\\:%])(\\.\\/)?((?:[\\w.-]+[
|
|
|
13
13
|
export function normalizeSkillDir(skillDir) {
|
|
14
14
|
return skillDir.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
15
15
|
}
|
|
16
|
-
export function skillEnvRef(platform = process.platform) {
|
|
17
|
-
return platform === 'win32' ? `%${EAP_SKILL_DIR_ENV}%` : `$${EAP_SKILL_DIR_ENV}`;
|
|
18
|
-
}
|
|
19
|
-
function asOptions(platformOrOpts) {
|
|
20
|
-
if (typeof platformOrOpts === 'string')
|
|
21
|
-
return { platform: platformOrOpts };
|
|
22
|
-
return platformOrOpts ?? {};
|
|
23
|
-
}
|
|
24
16
|
function defaultExists(absPath) {
|
|
25
17
|
try {
|
|
26
18
|
return existsSync(absPath);
|
|
@@ -34,12 +26,37 @@ function defaultExists(absPath) {
|
|
|
34
26
|
* 带目录或 `./` 的(`bin/foo.py`、`./main.py`、`scripts/a.py`)一律展开。
|
|
35
27
|
* 根上的裸文件名(`main.py`)仅当包内确实有该文件才展开,以免改写「写到用户目录再跑」的例子。
|
|
36
28
|
*/
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
29
|
+
function resolvePackageRelativePath(rel, skillDir, exists = defaultExists) {
|
|
30
|
+
const raw = rel.trim();
|
|
31
|
+
if (!raw || raw.startsWith('#') || raw.includes('://'))
|
|
32
|
+
return undefined;
|
|
33
|
+
if (path.isAbsolute(raw) || /^[A-Za-z]:[\\/]/.test(raw))
|
|
34
|
+
return undefined;
|
|
35
|
+
if (raw.startsWith('$') || raw.startsWith('%'))
|
|
36
|
+
return undefined;
|
|
37
|
+
const relNorm = raw.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
38
|
+
if (!relNorm || !/\.[A-Za-z0-9]{1,8}$/.test(relNorm))
|
|
39
|
+
return undefined;
|
|
40
|
+
const abs = path.resolve(skillDir, relNorm);
|
|
41
|
+
if (!exists(abs))
|
|
42
|
+
return undefined;
|
|
43
|
+
return normalizeSkillDir(abs);
|
|
44
|
+
}
|
|
45
|
+
function expandPackageFileRefs(body, skillDir, exists) {
|
|
46
|
+
return body.replace(/\]\(([^)]+)\)|`([^`\n]+)`/g, (full, linkDest, tickDest) => {
|
|
47
|
+
const raw = (linkDest ?? tickDest ?? '').trim();
|
|
48
|
+
const abs = resolvePackageRelativePath(raw, skillDir, exists);
|
|
49
|
+
if (!abs)
|
|
50
|
+
return full;
|
|
51
|
+
if (linkDest != null)
|
|
52
|
+
return `](${abs})`;
|
|
53
|
+
return `\`${abs}\``;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export function expandSkillScriptRefs(body, skillDir, opts) {
|
|
57
|
+
const exists = opts?.exists ?? defaultExists;
|
|
41
58
|
const dir = normalizeSkillDir(skillDir);
|
|
42
|
-
|
|
59
|
+
let expanded = body.replace(RELATIVE_CODE_PATH, (full, prefix, dotSlash, rel) => {
|
|
43
60
|
const relNorm = rel.replace(/\\/g, '/');
|
|
44
61
|
const packageRelative = Boolean(dotSlash) || relNorm.includes('/');
|
|
45
62
|
if (!packageRelative && !exists(path.join(skillDir, relNorm))) {
|
|
@@ -47,15 +64,10 @@ export function expandSkillScriptRefs(body, skillDir, platformOrOpts) {
|
|
|
47
64
|
}
|
|
48
65
|
return `${prefix}${dir}/${relNorm}`;
|
|
49
66
|
});
|
|
50
|
-
|
|
51
|
-
const envPath = platform === 'win32' ? `${envRef}\\<相对路径>` : `${envRef}/<相对路径>`;
|
|
52
|
-
const note = `技能目录(只读)环境变量 ${EAP_SKILL_DIR_ENV}=${dir}。` +
|
|
53
|
-
`包内任意文件用 ${envPath}(不限 scripts/)。` +
|
|
54
|
-
`读入正文里像包内脚本的相对路径已展开为绝对路径。` +
|
|
55
|
-
`当前工作目录是用户目录,相对路径写出在用户目录。\n\n`;
|
|
67
|
+
expanded = expandPackageFileRefs(expanded, skillDir, exists);
|
|
56
68
|
if (body.includes(`${EAP_SKILL_DIR_ENV}=`))
|
|
57
69
|
return expanded;
|
|
58
|
-
return
|
|
70
|
+
return `技能包根(只读)${dir}。\n\n` + expanded;
|
|
59
71
|
}
|
|
60
72
|
export function isSkillEntryFile(filePath) {
|
|
61
73
|
return /(^|[\\/])(SKILL\.md|skill\.md)$/i.test(filePath);
|