@gt-fe/eap-runtime 0.0.22 → 0.0.23

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.
@@ -16,7 +16,7 @@ import { inferHarnessSkillDir } from '../../tool/skill-dir-infer.js';
16
16
  import { isHitlDecision, unwrapHitlPaused } from '../hitl/hitl-handler.js';
17
17
  import { ToolConfirmationInterceptor } from '../../tool/tool-confirmation.js';
18
18
  import { resolveCheckpointThreadId } from '../../session/persist-turn-messages.js';
19
- import { toolTraceIdentity } from '../../tool/mcp-list.js';
19
+ import { mcpToolsetCode, toolTraceIdentity } from '../../tool/mcp-list.js';
20
20
  import { resolveExecutionTrace, warnTraceSpanFailure, } from '../execution-trace-context.js';
21
21
  import { emitRuntimeLog } from '../../common/logger.js';
22
22
  import { decideBeforeTool, } from '../../execution/interceptors.js';
@@ -92,6 +92,7 @@ export class ToolNode {
92
92
  // 4. 转换为 ToolCallRequest[](附带 manifest 路由字段)
93
93
  const requests = lastAssistantMessage.toolCalls.map((tc) => {
94
94
  const loaded = loadedTools?.[tc.name];
95
+ const toolsetCode = mcpToolsetCode(loaded);
95
96
  const correlationId = String(state['correlationId'] ?? tc.id);
96
97
  const taskId = String(state['taskId'] ?? state['correlationId'] ?? tc.id);
97
98
  const command = typeof tc.arguments?.command === 'string' ? tc.arguments.command : undefined;
@@ -114,6 +115,7 @@ export class ToolNode {
114
115
  // dev 上下文 DevMcpExecutor 直连所需;server 上下文会忽略
115
116
  execution: loaded?.manifest.execution,
116
117
  ...(loaded?.manifest.version ? { toolVersion: loaded.manifest.version } : {}),
118
+ ...(toolsetCode ? { toolsetCode } : {}),
117
119
  correlationId,
118
120
  taskId,
119
121
  ...(jail.workdir ? { jailWorkdir: jail.workdir } : {}),
@@ -506,10 +508,17 @@ export class ToolNode {
506
508
  const delegationToken = String(executionContext?.delegationToken ??
507
509
  state['delegationToken'] ??
508
510
  this.defaultAuthContext.delegationToken);
511
+ const fromState = typeof state['agentVersion'] === 'string' ? state['agentVersion'].trim() : '';
512
+ const agentDir = typeof state['agentDir'] === 'string' ? state['agentDir'].trim() : '';
513
+ const fromDir = agentDir
514
+ ? agentDir.split(/[/\\]/).filter(Boolean).pop()
515
+ : undefined;
516
+ const agentVersion = fromState || fromDir;
509
517
  return {
510
518
  userId,
511
519
  delegationToken,
512
520
  agentCode,
521
+ ...(agentVersion ? { agentVersion } : {}),
513
522
  };
514
523
  }
515
524
  shouldForceServer(state, toolName) {
@@ -4,7 +4,7 @@
4
4
  * 生产(server + EAP_PREINSTALL_ROOT):以预装为准,缺失则失败(04 Req 8.1)。
5
5
  * 开发态:Registry + 可选 ./tools 本地 manifest。
6
6
  */
7
- import { readFile } from 'node:fs/promises';
7
+ import { readdir, readFile } from 'node:fs/promises';
8
8
  import path from 'node:path';
9
9
  import { resolveAgentModelAllowlist, resolveAgentDefaultModel } from './agent-models.js';
10
10
  import { DEFAULT_HTTP_TOOL_TIMEOUT_SEC, DEFAULT_TOOL_TIMEOUT_SEC } from '../tool/timeout.js';
@@ -463,16 +463,71 @@ function fromInlineToolDeclaration(tool) {
463
463
  inputSchema: tool.input,
464
464
  };
465
465
  }
466
+ const DEV_LOCAL_MANIFEST_FILES = [
467
+ 'toolset.manifest.json',
468
+ 'tool.manifest.json',
469
+ 'manifest.json',
470
+ ];
471
+ async function readJsonObject(filePath) {
472
+ try {
473
+ const raw = await readFile(filePath, 'utf-8');
474
+ const parsed = JSON.parse(raw);
475
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
476
+ return null;
477
+ return parsed;
478
+ }
479
+ catch {
480
+ return null;
481
+ }
482
+ }
483
+ function loadedToolFromDevDir(dir, toolName, doc, toolsetOnly) {
484
+ const tools = doc.tools;
485
+ if (Array.isArray(tools) && tools.length > 0 && typeof tools[0] === 'object') {
486
+ const pre = {
487
+ toolId: String(doc.code ?? path.basename(dir)),
488
+ name: String(doc.name ?? path.basename(dir)),
489
+ version: String(doc.version ?? '0.0.0'),
490
+ manifest: doc,
491
+ bundlePath: dir,
492
+ };
493
+ const expanded = expandPreinstallToolset(pre);
494
+ return expanded.find((d) => d.name === toolName) ?? null;
495
+ }
496
+ if (toolsetOnly)
497
+ return null;
498
+ return fromLocalManifestFile(toolName, doc);
499
+ }
500
+ async function loadDevLocalManifestFromDir(dir, toolName, toolsetOnly) {
501
+ const files = toolsetOnly ? ['toolset.manifest.json'] : DEV_LOCAL_MANIFEST_FILES;
502
+ for (const file of files) {
503
+ const doc = await readJsonObject(path.join(dir, file));
504
+ if (!doc)
505
+ continue;
506
+ const hit = loadedToolFromDevDir(dir, toolName, doc, toolsetOnly);
507
+ if (hit)
508
+ return hit;
509
+ }
510
+ return null;
511
+ }
466
512
  async function loadDevLocalManifest(toolName, toolsDir) {
467
- const manifestPath = path.join(toolsDir, toolName, 'tool.manifest.json');
513
+ const direct = await loadDevLocalManifestFromDir(path.join(toolsDir, toolName), toolName, false);
514
+ if (direct)
515
+ return direct;
516
+ let entries;
468
517
  try {
469
- const raw = await readFile(manifestPath, 'utf-8');
470
- const manifest = JSON.parse(raw);
471
- return fromLocalManifestFile(toolName, manifest);
518
+ entries = await readdir(toolsDir, { withFileTypes: true });
472
519
  }
473
520
  catch {
474
521
  return null;
475
522
  }
523
+ for (const ent of entries) {
524
+ if (!ent.isDirectory() || ent.name === toolName)
525
+ continue;
526
+ const hit = await loadDevLocalManifestFromDir(path.join(toolsDir, ent.name), toolName, true);
527
+ if (hit)
528
+ return hit;
529
+ }
530
+ return null;
476
531
  }
477
532
  function fromLocalInstall(install) {
478
533
  return {
@@ -5,7 +5,7 @@
5
5
  * @see docs/contracts/cross-service-api.md §4.6
6
6
  */
7
7
  import { ErrorCodes } from '@gt-fe/eap-contracts';
8
- import { abortOrTimeoutMessage, isAbortError } from './timeout.js';
8
+ import { abortOrTimeoutMessage, isAbortError, timeoutMsFromExecution } from './timeout.js';
9
9
  function mapGatewayHttpError(status, body, statusText) {
10
10
  const code = body?.error?.code;
11
11
  const bodyMessage = [body?.error?.message, body?.message].find((s) => typeof s === 'string' && s.trim()) ?? '';
@@ -81,13 +81,17 @@ export class HttpToolExecutor {
81
81
  Authorization: `Bearer ${request.authContext.delegationToken}`,
82
82
  'X-Agent-Id': agentCode,
83
83
  'X-Agent-Code': agentCode,
84
+ ...(request.authContext.agentVersion
85
+ ? { 'X-Agent-Version': request.authContext.agentVersion }
86
+ : {}),
84
87
  'X-User-Id': request.authContext.userId,
85
88
  userId: request.authContext.userId,
86
89
  'X-Correlation-Id': correlationId,
87
90
  'X-Task-Id': taskId,
88
91
  };
89
92
  const controller = new AbortController();
90
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
93
+ const timeoutMs = timeoutMsFromExecution(request.execution, this.timeout);
94
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
91
95
  try {
92
96
  const response = await fetch(url, {
93
97
  method: 'POST',
@@ -137,7 +141,7 @@ export class HttpToolExecutor {
137
141
  }
138
142
  catch (error) {
139
143
  clearTimeout(timeoutId);
140
- const timeoutSec = Math.max(1, Math.round(this.timeout / 1000));
144
+ const timeoutSec = Math.max(1, Math.round(timeoutMsFromExecution(request.execution, this.timeout) / 1000));
141
145
  const aborted = isAbortError(error);
142
146
  return {
143
147
  toolCallId: request.toolCallId,
@@ -1,5 +1,5 @@
1
1
  export type { AuthContext, ToolCallRequest, ToolCallResult, LLMToolCall, LLMToolCallResponse, IToolExecutor, McpListedTool, } from './tool-executor.interface.js';
2
- export { hydrateMcpTools, isMcpDiscoverStub, needsMcpHydration, parseListedTools, toolsetCodeFromLoaded, toolTraceIdentity, } from './mcp-list.js';
2
+ export { assertScopedListEcho, hydrateMcpTools, isMcpDiscoverStub, mcpToolsetCode, needsMcpHydration, parseListedTools, toolsetCodeFromLoaded, toolTraceIdentity, } from './mcp-list.js';
3
3
  export { toLoadedToolMap, type LoadedToolDefinition, type LoadedToolMap, type LoadedToolManifestRef, } from './loaded-tool-definition.js';
4
4
  export { ToolOrchestrator } from './tool-orchestrator.js';
5
5
  export { isRetryableToolFailure, resolveToolRetryConfig, type ToolFailureShape, } from './tool-retry.js';
@@ -11,6 +11,7 @@ export { DevHttpExecutor, buildHttpToolUrl, resolveHttpExecutionFromRequest, typ
11
11
  export { DevToolExecutor, type DevToolExecutorOptions } from './dev-tool-executor.js';
12
12
  export { LocalMcpClient, type ILocalMcpClient, type LocalMcpInvokeContext, type LocalMcpInvokeResult, } from './local-mcp-client.js';
13
13
  export { fileArtifactFromToolOutput, extractFileReference, resolveFileArtifact, type FileArtifact, } from './file-artifact.js';
14
+ export { formatMcpOutput, toModelVisibleToolContent, MAX_MODEL_RESULT_ROWS, MAX_MODEL_CONTENT_CHARS, MAX_MODEL_RESULT_CHARS, } from './model-visible-tool-content.js';
14
15
  export { ToolExecutionRouter, type DeploymentContext, type ToolExecutionRouterOptions, } from './tool-execution-router.js';
15
16
  export { createToolExecutor, EmptyInstallStore, type CreateToolExecutorOptions, } from './create-tool-executor.js';
16
17
  export { assembleLoadedTools, mergeHarnessPrimitives as mergeBuiltinTools, withHarnessPrimitives as withBuiltinTools, HarnessPrimitiveExecutor as BuiltinToolExecutor, resolveUserDir, resolveJailFromState, captureOverlayJailOptions, collectToolBundlePaths, type HarnessJailOptions as BuiltinJailOptions, } from './harness-primitives.js';
@@ -1,4 +1,4 @@
1
- export { hydrateMcpTools, isMcpDiscoverStub, needsMcpHydration, parseListedTools, toolsetCodeFromLoaded, toolTraceIdentity, } from './mcp-list.js';
1
+ export { assertScopedListEcho, hydrateMcpTools, isMcpDiscoverStub, mcpToolsetCode, needsMcpHydration, parseListedTools, toolsetCodeFromLoaded, toolTraceIdentity, } from './mcp-list.js';
2
2
  export { toLoadedToolMap, } from './loaded-tool-definition.js';
3
3
  export { ToolOrchestrator } from './tool-orchestrator.js';
4
4
  export { isRetryableToolFailure, resolveToolRetryConfig, } from './tool-retry.js';
@@ -10,6 +10,7 @@ export { DevHttpExecutor, buildHttpToolUrl, resolveHttpExecutionFromRequest, } f
10
10
  export { DevToolExecutor } from './dev-tool-executor.js';
11
11
  export { LocalMcpClient, } from './local-mcp-client.js';
12
12
  export { fileArtifactFromToolOutput, extractFileReference, resolveFileArtifact, } from './file-artifact.js';
13
+ export { formatMcpOutput, toModelVisibleToolContent, MAX_MODEL_RESULT_ROWS, MAX_MODEL_CONTENT_CHARS, MAX_MODEL_RESULT_CHARS, } from './model-visible-tool-content.js';
13
14
  export { ToolExecutionRouter, } from './tool-execution-router.js';
14
15
  export { createToolExecutor, EmptyInstallStore, } from './create-tool-executor.js';
15
16
  export { assembleLoadedTools, mergeHarnessPrimitives as mergeBuiltinTools, withHarnessPrimitives as withBuiltinTools, HarnessPrimitiveExecutor as BuiltinToolExecutor, resolveUserDir, resolveJailFromState, captureOverlayJailOptions, collectToolBundlePaths, } from './harness-primitives.js';
@@ -21,7 +21,7 @@ export interface LoadedToolManifestRef {
21
21
  * MCP/HTTP 出站执行配置(来自 tool.manifest.execution)
22
22
  *
23
23
  * dev 上下文 DevMcpExecutor 消费此字段做直连(stdio / streamable_http / sse);
24
- * server 上下文 gateway 会忽略此字段,改由 foundation-service 侧根据 toolName 分派。
24
+ * server 上下文 Gateway 按 `params._meta.toolsetCode` / `version` 分派,不读清单 server。
25
25
  */
26
26
  execution?: unknown;
27
27
  }
@@ -7,6 +7,7 @@
7
7
  import type { McpRuntimeExecution } from '@gt-fe/eap-contracts';
8
8
  import { type FileArtifact } from './file-artifact.js';
9
9
  import type { McpListedTool } from './tool-executor.interface.js';
10
+ export { formatMcpOutput } from './model-visible-tool-content.js';
10
11
  export interface LocalMcpInvokeContext {
11
12
  /** Platform Token.sub / ExecutionContext.userId */
12
13
  userId?: string | null;
@@ -19,7 +20,6 @@ export interface ILocalMcpClient {
19
20
  invoke(execution: McpRuntimeExecution, args: Record<string, unknown>, context?: LocalMcpInvokeContext): Promise<LocalMcpInvokeResult>;
20
21
  list(execution: McpRuntimeExecution, context?: LocalMcpInvokeContext): Promise<McpListedTool[]>;
21
22
  }
22
- export declare function formatMcpOutput(result: unknown): string;
23
23
  export declare class LocalMcpClient implements ILocalMcpClient {
24
24
  invoke(execution: McpRuntimeExecution, args: Record<string, unknown>, context?: LocalMcpInvokeContext): Promise<LocalMcpInvokeResult>;
25
25
  list(execution: McpRuntimeExecution, context?: LocalMcpInvokeContext): Promise<McpListedTool[]>;
@@ -8,46 +8,9 @@ import { spawn } from 'node:child_process';
8
8
  import readline from 'node:readline';
9
9
  import { isAbortError, resolveToolTimeoutSec, toToolTimeoutMs, toolTimeoutErrorMessage, } from './timeout.js';
10
10
  import { fileArtifactFromToolOutput, fileBasesFromEnv } from './file-artifact.js';
11
+ import { formatMcpOutput } from './model-visible-tool-content.js';
11
12
  import { nextListCursor, parseListedTools } from './mcp-list.js';
12
- export function formatMcpOutput(result) {
13
- const r = result;
14
- if (r?.structuredContent !== undefined) {
15
- return JSON.stringify(redactFileLocators(r.structuredContent));
16
- }
17
- const text = r?.content?.[0]?.text;
18
- if (text) {
19
- try {
20
- return JSON.stringify(redactFileLocators(JSON.parse(text)));
21
- }
22
- catch {
23
- return text;
24
- }
25
- }
26
- return JSON.stringify(redactFileLocators(result));
27
- }
28
- function redactFileLocators(value) {
29
- if (Array.isArray(value))
30
- return value.map(redactFileLocators);
31
- if (!value || typeof value !== 'object')
32
- return value;
33
- const obj = value;
34
- const fileShaped = typeof obj.fileName === 'string'
35
- && (typeof obj.url === 'string' || typeof obj.relativePath === 'string');
36
- const out = {};
37
- for (const [key, child] of Object.entries(obj)) {
38
- if (key === 'fileReference' && child && typeof child === 'object' && !Array.isArray(child)) {
39
- const ref = { ...child };
40
- delete ref.url;
41
- delete ref.relativePath;
42
- out[key] = ref;
43
- continue;
44
- }
45
- if (fileShaped && (key === 'url' || key === 'relativePath'))
46
- continue;
47
- out[key] = redactFileLocators(child);
48
- }
49
- return out;
50
- }
13
+ export { formatMcpOutput } from './model-visible-tool-content.js';
51
14
  function expandEnvRefs(value) {
52
15
  if (!value)
53
16
  return '';
@@ -5,7 +5,7 @@
5
5
  * 或 request.execution 带可解析的 code bundlePath+handler,则直接本机执行。
6
6
  */
7
7
  import { spawn } from 'node:child_process';
8
- import { existsSync } from 'node:fs';
8
+ import { existsSync, mkdirSync } from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import { ErrorCodes, resolveReadyInstallByToolName, resolveMcpExecutionFromInstall, resolveCodeHandlerFromInstall, } from '@gt-fe/eap-contracts';
11
11
  import { isAbortError, toToolTimeoutMs, toolTimeoutErrorMessage } from './timeout.js';
@@ -49,13 +49,21 @@ function formatCodeLaunchError(err, spec) {
49
49
  }
50
50
  return new Error(err instanceof Error ? err.message : String(err));
51
51
  }
52
- function runHandler(handlerPath, input, timeoutMs, execution) {
52
+ function runHandler(handlerPath, input, timeoutMs, execution, spawnOpts) {
53
53
  if (!existsSync(handlerPath)) {
54
54
  return Promise.reject(new Error(`找不到 handler 文件:${handlerPath}。请检查 toolset.manifest.json 的 tools[].handler 与工具目录。`));
55
55
  }
56
56
  const { cmd, args } = resolveCommand(handlerPath, execution);
57
+ const cwd = spawnOpts?.cwd?.trim() || undefined;
58
+ if (cwd)
59
+ mkdirSync(cwd, { recursive: true });
60
+ const env = { ...process.env, ...(spawnOpts?.env ?? {}) };
61
+ if (spawnOpts?.userId)
62
+ env.EAP_USER_ID = spawnOpts.userId;
63
+ if (cwd)
64
+ env.EAP_ARTIFACT_ROOT = cwd;
57
65
  return new Promise((resolve, reject) => {
58
- const child = spawn(cmd, args, { stdio: ['pipe', 'pipe', 'pipe'] });
66
+ const child = spawn(cmd, args, { stdio: ['pipe', 'pipe', 'pipe'], cwd, env });
59
67
  let stdout = '';
60
68
  let stderr = '';
61
69
  let settled = false;
@@ -92,6 +100,20 @@ function runHandler(handlerPath, input, timeoutMs, execution) {
92
100
  child.stdin.end();
93
101
  });
94
102
  }
103
+ function executionEnvFromRequest(request) {
104
+ const raw = request.execution;
105
+ if (!raw || typeof raw !== 'object')
106
+ return undefined;
107
+ const env = raw.env;
108
+ if (!env || typeof env !== 'object' || Array.isArray(env))
109
+ return undefined;
110
+ const out = {};
111
+ for (const [key, value] of Object.entries(env)) {
112
+ if (typeof value === 'string')
113
+ out[key] = value;
114
+ }
115
+ return Object.keys(out).length > 0 ? out : undefined;
116
+ }
95
117
  /** 是否为应本机执行的 code(显式 localPreferred,例如 DevToolExecutor) */
96
118
  export function isLocalPreferredExecution(raw) {
97
119
  if (!raw || typeof raw !== 'object')
@@ -188,14 +210,19 @@ export class LocalToolExecutor {
188
210
  }
189
211
  async executeCodeSpec(request, spec) {
190
212
  try {
191
- const stdout = await runHandler(spec.handlerPath, request.arguments, spec.timeoutMs ?? this.options.timeoutMs ?? 30_000, { runtime: spec.runtime, args: spec.args });
213
+ const jail = request.jailWorkdir?.trim() || undefined;
214
+ const stdout = await runHandler(spec.handlerPath, request.arguments, spec.timeoutMs ?? this.options.timeoutMs ?? 30_000, { runtime: spec.runtime, args: spec.args }, {
215
+ cwd: jail,
216
+ userId: request.authContext?.userId,
217
+ env: executionEnvFromRequest(request),
218
+ });
192
219
  return {
193
220
  toolCallId: request.toolCallId,
194
221
  toolName: request.toolName,
195
222
  content: stdout,
196
223
  success: true,
197
224
  runtime: 'local',
198
- file: fileArtifactFromToolOutput(stdout, fileBasesFromEnv()),
225
+ file: fileArtifactFromToolOutput(stdout, [...fileBasesFromEnv(), jail]),
199
226
  };
200
227
  }
201
228
  catch (error) {
@@ -8,6 +8,14 @@ import type { LoadedToolDefinition, LoadedToolMap } from './loaded-tool-definiti
8
8
  import type { AuthContext, IToolExecutor, McpListedTool } from './tool-executor.interface.js';
9
9
  export declare function parseListedTools(result: unknown): McpListedTool[];
10
10
  export declare function nextListCursor(result: unknown): string | undefined;
11
+ /**
12
+ * 有 toolsetCode 的 tools/list:回包 `_meta` 必须与请求作用域一致(Gateway A16)。
13
+ * 无 `_meta` 或编码/版本不符视为本次 list 失败,不把全局目录当该工具集。
14
+ */
15
+ export declare function assertScopedListEcho(result: unknown, expected: {
16
+ toolsetCode: string;
17
+ version?: string;
18
+ }): void;
11
19
  export interface McpDiscoverExecution {
12
20
  protocol?: string;
13
21
  discover?: boolean;
@@ -32,6 +40,19 @@ export declare function toolsetCodeFromLoaded(loaded: {
32
40
  id?: string;
33
41
  };
34
42
  } | undefined): string | undefined;
43
+ /**
44
+ * Gateway `params._meta.toolsetCode`:hydrate 后的 `toolset:toolName` 前缀,
45
+ * 或 MCP discover stub 自身的工具集编码。
46
+ */
47
+ export declare function mcpToolsetCode(loaded: {
48
+ toolId?: string;
49
+ name?: string;
50
+ manifest?: {
51
+ id?: string;
52
+ type?: string;
53
+ execution?: unknown;
54
+ };
55
+ } | undefined): string | undefined;
35
56
  /** Tool Span:工具集 code + 声明版本。与 Skill 的 skillSpace/version 同一假设。 */
36
57
  export declare function toolTraceIdentity(loaded: {
37
58
  toolId?: string;
@@ -16,8 +16,9 @@ export function parseListedTools(result) {
16
16
  const t = raw;
17
17
  if (typeof t.name !== 'string' || !t.name.trim())
18
18
  continue;
19
- const inputSchema = t.inputSchema && typeof t.inputSchema === 'object' && !Array.isArray(t.inputSchema)
20
- ? t.inputSchema
19
+ const schemaRaw = t.inputSchema ?? t.input;
20
+ const inputSchema = schemaRaw && typeof schemaRaw === 'object' && !Array.isArray(schemaRaw)
21
+ ? schemaRaw
21
22
  : undefined;
22
23
  out.push({
23
24
  name: t.name,
@@ -31,6 +32,30 @@ export function nextListCursor(result) {
31
32
  const cursor = result?.nextCursor;
32
33
  return typeof cursor === 'string' && cursor.trim() ? cursor : undefined;
33
34
  }
35
+ /**
36
+ * 有 toolsetCode 的 tools/list:回包 `_meta` 必须与请求作用域一致(Gateway A16)。
37
+ * 无 `_meta` 或编码/版本不符视为本次 list 失败,不把全局目录当该工具集。
38
+ */
39
+ export function assertScopedListEcho(result, expected) {
40
+ const root = result && typeof result === 'object' && !Array.isArray(result)
41
+ ? result
42
+ : {};
43
+ const echo = root._meta && typeof root._meta === 'object' && !Array.isArray(root._meta)
44
+ ? root._meta
45
+ : {};
46
+ const code = typeof echo.toolsetCode === 'string' ? echo.toolsetCode.trim() : '';
47
+ const version = typeof echo.version === 'string' ? echo.version.trim() : '';
48
+ const wantCode = expected.toolsetCode.trim();
49
+ const wantVersion = expected.version?.trim() ?? '';
50
+ if (!wantCode)
51
+ return;
52
+ if (code !== wantCode) {
53
+ throw new Error(`MCP tools/list 回包 toolsetCode 与请求不一致: 期望 ${wantCode}, 实际 ${code || '<missing>'}`);
54
+ }
55
+ if (wantVersion && version !== wantVersion) {
56
+ throw new Error(`MCP tools/list 回包 version 与请求不一致: 期望 ${wantVersion}, 实际 ${version || '<missing>'}`);
57
+ }
58
+ }
34
59
  export function readMcpDiscoverExecution(raw) {
35
60
  if (!raw || typeof raw !== 'object' || Array.isArray(raw))
36
61
  return null;
@@ -84,6 +109,21 @@ export function toolsetCodeFromLoaded(loaded) {
84
109
  }
85
110
  return undefined;
86
111
  }
112
+ /**
113
+ * Gateway `params._meta.toolsetCode`:hydrate 后的 `toolset:toolName` 前缀,
114
+ * 或 MCP discover stub 自身的工具集编码。
115
+ */
116
+ export function mcpToolsetCode(loaded) {
117
+ const fromPrefix = toolsetCodeFromLoaded(loaded);
118
+ if (fromPrefix)
119
+ return fromPrefix;
120
+ if (!loaded || loaded.manifest?.type !== 'mcp')
121
+ return undefined;
122
+ if (!readMcpDiscoverExecution(loaded.manifest.execution))
123
+ return undefined;
124
+ const id = (loaded.manifest.id || loaded.toolId || loaded.name || '').trim();
125
+ return id || undefined;
126
+ }
87
127
  function declaredToolVersion(toolName, version) {
88
128
  const raw = version?.trim();
89
129
  if (!raw || raw === toolName)
@@ -120,7 +160,10 @@ export async function hydrateMcpTools(loadedTools, executor, auth) {
120
160
  }
121
161
  const groups = new Map();
122
162
  for (const stub of stubs) {
123
- const key = mcpEndpointKey(stub.manifest.execution);
163
+ const endpoint = mcpEndpointKey(stub.manifest.execution);
164
+ const toolsetCode = mcpToolsetCode(stub) ?? stub.name;
165
+ const version = stub.manifest.version?.trim() ?? '';
166
+ const key = `${endpoint}\0${toolsetCode}\0${version}`;
124
167
  const list = groups.get(key);
125
168
  if (list)
126
169
  list.push(stub);
@@ -132,6 +175,7 @@ export async function hydrateMcpTools(loadedTools, executor, auth) {
132
175
  for (const [, group] of groups) {
133
176
  const primary = group[0];
134
177
  const endpoint = mcpEndpointKey(primary.manifest.execution, primary.name);
178
+ const toolsetCode = mcpToolsetCode(primary);
135
179
  let listed;
136
180
  try {
137
181
  listed = await executor.listTools({
@@ -144,6 +188,8 @@ export async function hydrateMcpTools(loadedTools, executor, auth) {
144
188
  toolType: 'mcp',
145
189
  toolRuntime: primary.manifest.toolRuntime,
146
190
  execution: primary.manifest.execution,
191
+ ...(toolsetCode ? { toolsetCode } : {}),
192
+ ...(primary.manifest.version ? { toolVersion: primary.manifest.version } : {}),
147
193
  });
148
194
  }
149
195
  catch (err) {
@@ -18,7 +18,7 @@ export declare class McpToolExecutor implements IToolExecutor {
18
18
  private readonly mcpUrl;
19
19
  private rpcId;
20
20
  private initialized;
21
- /** endpoint+user+agent → in-flight / completed tools/list */
21
+ /** endpoint+user+agent+toolset+version → in-flight / completed tools/list */
22
22
  private readonly listCache;
23
23
  constructor(options: McpToolExecutorOptions);
24
24
  execute(request: ToolCallRequest): Promise<ToolCallResult>;
@@ -5,8 +5,10 @@
5
5
  */
6
6
  import { ErrorCodes } from '@gt-fe/eap-contracts';
7
7
  import { emitRuntimeLog } from '../common/logger.js';
8
- import { abortOrTimeoutMessage, isAbortError } from './timeout.js';
9
- import { nextListCursor, parseListedTools } from './mcp-list.js';
8
+ import { abortOrTimeoutMessage, isAbortError, timeoutMsFromExecution } from './timeout.js';
9
+ import { formatMcpOutput } from './model-visible-tool-content.js';
10
+ import { fileArtifactFromToolOutput, fileBasesFromEnv } from './file-artifact.js';
11
+ import { assertScopedListEcho, nextListCursor, parseListedTools } from './mcp-list.js';
10
12
  /** Tool Gateway 基址:必须是绝对 http(s)。空串 / 相对路径禁止 fetch。 */
11
13
  export function isAbsoluteHttpUrl(value) {
12
14
  if (typeof value !== 'string')
@@ -26,10 +28,7 @@ function resolveAgentCode(request) {
26
28
  return request.authContext.agentCode;
27
29
  }
28
30
  function formatContent(result) {
29
- const text = result?.content?.[0]?.text;
30
- if (text != null)
31
- return text;
32
- return JSON.stringify(result ?? {});
31
+ return formatMcpOutput(result ?? {});
33
32
  }
34
33
  function mapMcpError(status, body) {
35
34
  const eapCode = body?.error?.data?.eapCode;
@@ -56,13 +55,41 @@ function mapMcpError(status, body) {
56
55
  }
57
56
  return { error: message, errorCode: eapCode ?? 'EXECUTION_FAILED', retryable: false };
58
57
  }
58
+ /** Gateway 作用域只在 params._meta;name / arguments / cursor 是协议字段。 */
59
+ function mcpScopeMeta(request) {
60
+ const toolsetCode = request.toolsetCode?.trim();
61
+ const version = request.toolVersion?.trim();
62
+ const meta = {};
63
+ if (toolsetCode)
64
+ meta.toolsetCode = toolsetCode;
65
+ if (version)
66
+ meta.version = version;
67
+ return Object.keys(meta).length > 0 ? meta : undefined;
68
+ }
69
+ function mcpCallParams(request) {
70
+ const _meta = mcpScopeMeta(request);
71
+ return {
72
+ name: request.toolName,
73
+ arguments: request.arguments,
74
+ ...(_meta ? { _meta } : {}),
75
+ };
76
+ }
77
+ function mcpListCacheKey(mcpUrl, request) {
78
+ return [
79
+ mcpUrl,
80
+ request.authContext.userId ?? '',
81
+ request.authContext.agentCode,
82
+ request.toolsetCode?.trim() ?? '',
83
+ request.toolVersion?.trim() ?? '',
84
+ ].join('\0');
85
+ }
59
86
  export class McpToolExecutor {
60
87
  baseUrl;
61
88
  timeout;
62
89
  mcpUrl;
63
90
  rpcId = 1;
64
91
  initialized = false;
65
- /** endpoint+user+agent → in-flight / completed tools/list */
92
+ /** endpoint+user+agent+toolset+version → in-flight / completed tools/list */
66
93
  listCache = new Map();
67
94
  constructor(options) {
68
95
  if (!isAbsoluteHttpUrl(options.baseUrl)) {
@@ -87,16 +114,7 @@ export class McpToolExecutor {
87
114
  jsonrpc: '2.0',
88
115
  id,
89
116
  method: 'tools/call',
90
- params: {
91
- name: request.toolName,
92
- arguments: request.arguments,
93
- ...(request.toolVersion
94
- ? {
95
- version: request.toolVersion,
96
- _meta: { version: request.toolVersion },
97
- }
98
- : {}),
99
- },
117
+ params: mcpCallParams(request),
100
118
  }, request, agentCode, correlationId);
101
119
  if (!response.ok) {
102
120
  let body = null;
@@ -128,16 +146,18 @@ export class McpToolExecutor {
128
146
  ...mapped,
129
147
  };
130
148
  }
149
+ const raw = body.result;
131
150
  return {
132
151
  toolCallId: request.toolCallId,
133
152
  toolName: request.toolName,
134
- content: formatContent(body.result),
153
+ content: formatContent(raw),
135
154
  success: true,
136
155
  runtime: 'server',
156
+ file: fileArtifactFromToolOutput(raw?.structuredContent ?? raw?.content?.[0]?.text ?? raw, [...fileBasesFromEnv(), request.jailWorkdir]),
137
157
  };
138
158
  }
139
159
  catch (err) {
140
- const timeoutSec = Math.max(1, Math.round(this.timeout / 1000));
160
+ const timeoutSec = Math.max(1, Math.round(timeoutMsFromExecution(request.execution, this.timeout) / 1000));
141
161
  const aborted = isAbortError(err);
142
162
  return {
143
163
  toolCallId: request.toolCallId,
@@ -152,7 +172,7 @@ export class McpToolExecutor {
152
172
  }
153
173
  }
154
174
  async listTools(request) {
155
- const key = `${this.mcpUrl}\0${request.authContext.userId}\0${request.authContext.agentCode}`;
175
+ const key = mcpListCacheKey(this.mcpUrl, request);
156
176
  const cached = this.listCache.get(key);
157
177
  if (cached)
158
178
  return cached;
@@ -168,10 +188,22 @@ export class McpToolExecutor {
168
188
  }
169
189
  async listAllTools(request) {
170
190
  await this.ensureInitialized(request);
191
+ const meta = mcpScopeMeta(request);
192
+ if (meta?.toolsetCode) {
193
+ const result = await this.rpcResult(request, 'tools/list', { _meta: meta });
194
+ assertScopedListEcho(result, {
195
+ toolsetCode: meta.toolsetCode,
196
+ version: meta.version,
197
+ });
198
+ return parseListedTools(result);
199
+ }
171
200
  const tools = [];
172
201
  let cursor;
173
202
  do {
174
- const result = await this.rpcResult(request, 'tools/list', cursor ? { cursor } : {});
203
+ const result = await this.rpcResult(request, 'tools/list', {
204
+ ...(cursor ? { cursor } : {}),
205
+ ...(meta ? { _meta: meta } : {}),
206
+ });
175
207
  tools.push(...parseListedTools(result));
176
208
  cursor = nextListCursor(result);
177
209
  } while (cursor);
@@ -223,7 +255,8 @@ export class McpToolExecutor {
223
255
  }
224
256
  async postRpc(payload, request, agentCode, correlationId) {
225
257
  const controller = new AbortController();
226
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
258
+ const timeoutMs = timeoutMsFromExecution(request.execution, this.timeout);
259
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
227
260
  try {
228
261
  return await fetch(this.mcpUrl, {
229
262
  method: 'POST',
@@ -234,6 +267,9 @@ export class McpToolExecutor {
234
267
  Authorization: `Bearer ${request.authContext.delegationToken}`,
235
268
  'X-Agent-Code': agentCode,
236
269
  'X-Agent-Id': agentCode,
270
+ ...(request.authContext.agentVersion
271
+ ? { 'X-Agent-Version': request.authContext.agentVersion }
272
+ : {}),
237
273
  'X-User-Id': request.authContext.userId,
238
274
  // 上游 mcp-data-service 认 `userId`;Gateway 侧亦保留 X-User-Id
239
275
  userId: request.authContext.userId,
@@ -0,0 +1,16 @@
1
+ /**
2
+ * 模型可见 Tool 结果:MCP / Code 成功正文在写入 GraphState 前统一裁到最多 200 行,并写 recordCount / previewCount / truncated。
3
+ *
4
+ * 假设:模型看到的永远可能是预览;全量条数与是否裁过只在这一处写。
5
+ * 不按工具名分支。fileReference 路径仍由 format 阶段剔除,下载走 result.file。
6
+ *
7
+ * 预览行数按 DeepSeek V4 Pro(约 1M 上下文)估:单次工具结果要够看列和分布,
8
+ * 又不能看起来像全表。200 行约 3–8 万 token(视列宽);多轮 5–6 次查询仍远低于窗口。
9
+ * 50 行只够看样例。1000 行接近聚合工具自己的预览上限,模型容易拿来排名。
10
+ * 超宽表先按行裁,再按体积把行数压到整段 JSON 仍合法。
11
+ */
12
+ export declare const MAX_MODEL_RESULT_ROWS = 200;
13
+ export declare const MAX_MODEL_CONTENT_CHARS = 64000;
14
+ export declare const MAX_MODEL_RESULT_CHARS = 120000;
15
+ export declare function formatMcpOutput(result: unknown): string;
16
+ export declare function toModelVisibleToolContent(content: string): string;
@@ -0,0 +1,266 @@
1
+ /**
2
+ * 模型可见 Tool 结果:MCP / Code 成功正文在写入 GraphState 前统一裁到最多 200 行,并写 recordCount / previewCount / truncated。
3
+ *
4
+ * 假设:模型看到的永远可能是预览;全量条数与是否裁过只在这一处写。
5
+ * 不按工具名分支。fileReference 路径仍由 format 阶段剔除,下载走 result.file。
6
+ *
7
+ * 预览行数按 DeepSeek V4 Pro(约 1M 上下文)估:单次工具结果要够看列和分布,
8
+ * 又不能看起来像全表。200 行约 3–8 万 token(视列宽);多轮 5–6 次查询仍远低于窗口。
9
+ * 50 行只够看样例。1000 行接近聚合工具自己的预览上限,模型容易拿来排名。
10
+ * 超宽表先按行裁,再按体积把行数压到整段 JSON 仍合法。
11
+ */
12
+ export const MAX_MODEL_RESULT_ROWS = 200;
13
+ export const MAX_MODEL_CONTENT_CHARS = 64_000;
14
+ export const MAX_MODEL_RESULT_CHARS = 120_000;
15
+ const PRIMARY_ARRAY_KEYS = ['rows', 'data', 'records', 'result', 'dataset', 'items', 'results'];
16
+ const TEXT_CLIP_SUFFIX = '...[文本已截断]';
17
+ export function formatMcpOutput(result) {
18
+ const r = result;
19
+ const count = finiteCount(r?._meta?.recordCount) ?? finiteCount(r?._meta?.totalCount);
20
+ if (r?.structuredContent !== undefined) {
21
+ return JSON.stringify(attachCount(redactFileLocators(r.structuredContent), count));
22
+ }
23
+ const text = r?.content?.[0]?.text;
24
+ if (text) {
25
+ try {
26
+ return JSON.stringify(attachCount(redactFileLocators(JSON.parse(text)), count));
27
+ }
28
+ catch {
29
+ if (count == null)
30
+ return text;
31
+ return JSON.stringify({ recordCount: count, content: text });
32
+ }
33
+ }
34
+ return JSON.stringify(attachCount(redactFileLocators(result), count));
35
+ }
36
+ export function toModelVisibleToolContent(content) {
37
+ if (content === '')
38
+ return content;
39
+ const parsed = parseJson(content);
40
+ if (parsed === undefined) {
41
+ if (content.length <= MAX_MODEL_CONTENT_CHARS)
42
+ return content;
43
+ return `${content.slice(0, MAX_MODEL_CONTENT_CHARS)}${TEXT_CLIP_SUFFIX}`;
44
+ }
45
+ const clipped = clipParsed(parsed);
46
+ if (!clipped.needEnvelope)
47
+ return content;
48
+ let out = clipped.value;
49
+ if (isPlainObject(clipped.value)) {
50
+ out = {
51
+ ...clipped.value,
52
+ recordCount: clipped.recordCount,
53
+ previewCount: clipped.previewCount,
54
+ truncated: clipped.truncated,
55
+ };
56
+ }
57
+ else if (Array.isArray(clipped.value)) {
58
+ out = {
59
+ truncated: clipped.truncated,
60
+ recordCount: clipped.recordCount,
61
+ previewCount: clipped.previewCount,
62
+ data: clipped.value,
63
+ };
64
+ }
65
+ if (typeof out === 'string') {
66
+ if (out.length <= MAX_MODEL_CONTENT_CHARS)
67
+ return JSON.stringify(out);
68
+ return JSON.stringify(`${out.slice(0, MAX_MODEL_CONTENT_CHARS)}${TEXT_CLIP_SUFFIX}`);
69
+ }
70
+ return stringifyWithinLimit(out, clipped.recordCount);
71
+ }
72
+ function clipParsed(value) {
73
+ if (Array.isArray(value)) {
74
+ const recordCount = value.length;
75
+ const clipped = value.length > MAX_MODEL_RESULT_ROWS;
76
+ const next = clipped ? value.slice(0, MAX_MODEL_RESULT_ROWS) : value;
77
+ return {
78
+ value: next,
79
+ needEnvelope: clipped,
80
+ recordCount,
81
+ previewCount: next.length,
82
+ truncated: clipped,
83
+ };
84
+ }
85
+ if (isPlainObject(value)) {
86
+ const obj = value;
87
+ const next = {};
88
+ let didClip = false;
89
+ for (const [key, child] of Object.entries(obj)) {
90
+ if (typeof child === 'string' && child.length > MAX_MODEL_CONTENT_CHARS) {
91
+ next[key] = `${child.slice(0, MAX_MODEL_CONTENT_CHARS)}${TEXT_CLIP_SUFFIX}`;
92
+ didClip = true;
93
+ }
94
+ else if (Array.isArray(child) && isPrimaryArrayKey(key) && child.length > MAX_MODEL_RESULT_ROWS) {
95
+ next[key] = child.slice(0, MAX_MODEL_RESULT_ROWS);
96
+ didClip = true;
97
+ }
98
+ else {
99
+ next[key] = child;
100
+ }
101
+ }
102
+ const prePrimary = primaryArrayLength(obj);
103
+ const recordCount = finiteCount(obj.recordCount) ?? finiteCount(obj.rowCount) ?? prePrimary;
104
+ const previewCount = primaryArrayLength(next);
105
+ const truncated = didClip
106
+ || obj.truncated === true
107
+ || (recordCount != null && previewCount != null && previewCount < recordCount);
108
+ const needEnvelope = didClip
109
+ || truncated
110
+ || finiteCount(obj.recordCount) != null
111
+ || finiteCount(obj.rowCount) != null;
112
+ return {
113
+ value: next,
114
+ needEnvelope,
115
+ recordCount,
116
+ previewCount,
117
+ truncated,
118
+ };
119
+ }
120
+ if (typeof value === 'string' && value.length > MAX_MODEL_CONTENT_CHARS) {
121
+ return {
122
+ value: `${value.slice(0, MAX_MODEL_CONTENT_CHARS)}${TEXT_CLIP_SUFFIX}`,
123
+ needEnvelope: false,
124
+ truncated: true,
125
+ };
126
+ }
127
+ return { value, needEnvelope: false, truncated: false };
128
+ }
129
+ function stringifyWithinLimit(value, recordCount) {
130
+ let current = value;
131
+ let json = JSON.stringify(current);
132
+ if (json.length <= MAX_MODEL_RESULT_CHARS)
133
+ return json;
134
+ if (isPlainObject(current)) {
135
+ current = shrinkPrimaryArray(current, recordCount);
136
+ json = JSON.stringify(current);
137
+ if (json.length <= MAX_MODEL_RESULT_CHARS)
138
+ return json;
139
+ const shortened = clipLongStrings(current, 256);
140
+ json = JSON.stringify(shortened);
141
+ if (json.length <= MAX_MODEL_RESULT_CHARS)
142
+ return json;
143
+ const count = isPlainObject(shortened)
144
+ ? (finiteCount(shortened.recordCount) ?? recordCount)
145
+ : recordCount;
146
+ json = JSON.stringify({
147
+ truncated: true,
148
+ recordCount: count,
149
+ previewCount: 0,
150
+ message: '结果过大,模型侧不返回行数据;请用 aggregate_dataset_data / export_dataset_data 处理全量',
151
+ });
152
+ }
153
+ if (json.length <= MAX_MODEL_RESULT_CHARS)
154
+ return json;
155
+ return JSON.stringify({ truncated: true, previewCount: 0, message: '结果过大' });
156
+ }
157
+ function clipLongStrings(value, maxChars) {
158
+ if (typeof value === 'string') {
159
+ return value.length > maxChars ? `${value.slice(0, maxChars)}${TEXT_CLIP_SUFFIX}` : value;
160
+ }
161
+ if (Array.isArray(value))
162
+ return value.map((item) => clipLongStrings(item, maxChars));
163
+ if (!isPlainObject(value))
164
+ return value;
165
+ const out = {};
166
+ for (const [key, child] of Object.entries(value)) {
167
+ out[key] = clipLongStrings(child, maxChars);
168
+ }
169
+ return out;
170
+ }
171
+ function isPrimaryArrayKey(key) {
172
+ return PRIMARY_ARRAY_KEYS.includes(key);
173
+ }
174
+ function shrinkPrimaryArray(obj, recordCount) {
175
+ const key = primaryArrayKey(obj);
176
+ const rows = key ? obj[key] : undefined;
177
+ if (!key || !Array.isArray(rows) || rows.length <= 1)
178
+ return obj;
179
+ let low = 1;
180
+ let high = rows.length;
181
+ let best = 1;
182
+ while (low <= high) {
183
+ const mid = Math.floor((low + high) / 2);
184
+ const candidate = withPreview(obj, key, rows.slice(0, mid), recordCount);
185
+ if (JSON.stringify(candidate).length <= MAX_MODEL_RESULT_CHARS) {
186
+ best = mid;
187
+ low = mid + 1;
188
+ }
189
+ else {
190
+ high = mid - 1;
191
+ }
192
+ }
193
+ return withPreview(obj, key, rows.slice(0, best), recordCount);
194
+ }
195
+ function withPreview(obj, key, rows, recordCount) {
196
+ const full = recordCount ?? finiteCount(obj.recordCount) ?? finiteCount(obj.rowCount) ?? rows.length;
197
+ return {
198
+ ...obj,
199
+ [key]: rows,
200
+ recordCount: full,
201
+ previewCount: rows.length,
202
+ truncated: true,
203
+ };
204
+ }
205
+ function primaryArrayKey(obj) {
206
+ for (const key of PRIMARY_ARRAY_KEYS) {
207
+ if (Array.isArray(obj[key]))
208
+ return key;
209
+ }
210
+ return undefined;
211
+ }
212
+ function attachCount(value, count) {
213
+ if (count == null || !isPlainObject(value))
214
+ return value;
215
+ const obj = value;
216
+ if (finiteCount(obj.recordCount) != null || finiteCount(obj.rowCount) != null)
217
+ return value;
218
+ return { ...obj, recordCount: count };
219
+ }
220
+ function primaryArrayLength(obj) {
221
+ for (const key of PRIMARY_ARRAY_KEYS) {
222
+ const child = obj[key];
223
+ if (Array.isArray(child))
224
+ return child.length;
225
+ }
226
+ return undefined;
227
+ }
228
+ function finiteCount(value) {
229
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
230
+ }
231
+ function parseJson(text) {
232
+ const trimmed = text.trim();
233
+ if (!trimmed.startsWith('{') && !trimmed.startsWith('['))
234
+ return undefined;
235
+ try {
236
+ return JSON.parse(trimmed);
237
+ }
238
+ catch {
239
+ return undefined;
240
+ }
241
+ }
242
+ function isPlainObject(value) {
243
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
244
+ }
245
+ function redactFileLocators(value) {
246
+ if (Array.isArray(value))
247
+ return value.map(redactFileLocators);
248
+ if (!isPlainObject(value))
249
+ return value;
250
+ const obj = value;
251
+ const fileShaped = typeof obj.fileName === 'string'
252
+ && (typeof obj.url === 'string' || typeof obj.relativePath === 'string');
253
+ const out = {};
254
+ for (const [key, child] of Object.entries(obj)) {
255
+ if (key === 'fileReference' && isPlainObject(child)) {
256
+ const ref = { ...child };
257
+ delete ref.url;
258
+ out[key] = ref;
259
+ continue;
260
+ }
261
+ if (fileShaped && key === 'url')
262
+ continue;
263
+ out[key] = redactFileLocators(child);
264
+ }
265
+ return out;
266
+ }
@@ -1,14 +1,7 @@
1
1
  /**
2
- * Tool / toolset `timeout`:秒。
3
- * 与 `@gt-fe/eap-contracts` `src/tool/timeout.ts` 同语义;Runtime 钉的是 contracts 0.0.9,
4
- * 尚未含这些导出。发版 contracts 后改为从 `@gt-fe/eap-contracts` 引用。
2
+ * 秒钳制与 abort 识别从 `@gt-fe/eap-contracts` 来。
3
+ * 本文件保留:本次工具 execution.timeout → 毫秒。
5
4
  */
6
- export declare const DEFAULT_TOOL_TIMEOUT_SEC = 30;
7
- export declare const DEFAULT_HTTP_TOOL_TIMEOUT_SEC = 10;
8
- export declare const MIN_TOOL_TIMEOUT_SEC = 1;
9
- export declare const MAX_TOOL_TIMEOUT_SEC = 300;
10
- export declare function resolveToolTimeoutSec(timeoutSec: unknown, fallbackSec?: number): number;
11
- export declare function toToolTimeoutMs(timeoutSec: unknown, fallbackSec?: number): number;
12
- export declare function isAbortError(error: unknown): boolean;
13
- export declare function toolTimeoutErrorMessage(timeoutSec: number): string;
14
- export declare function abortOrTimeoutMessage(error: unknown, timeoutSec: number): string;
5
+ export { DEFAULT_HTTP_TOOL_TIMEOUT_SEC, DEFAULT_TOOL_TIMEOUT_SEC, MAX_TOOL_TIMEOUT_SEC, MIN_TOOL_TIMEOUT_SEC, abortOrTimeoutMessage, isAbortError, resolveToolTimeoutSec, toToolTimeoutMs, toolTimeoutErrorMessage, } from '@gt-fe/eap-contracts';
6
+ /** 清单 tools[].timeout / 展开后 execution.timeout(秒)→ 本次调用毫秒;没有则用装配默认。 */
7
+ export declare function timeoutMsFromExecution(execution: unknown, fallbackMs: number): number;
@@ -1,39 +1,17 @@
1
1
  /**
2
- * Tool / toolset `timeout`:秒。
3
- * 与 `@gt-fe/eap-contracts` `src/tool/timeout.ts` 同语义;Runtime 钉的是 contracts 0.0.9,
4
- * 尚未含这些导出。发版 contracts 后改为从 `@gt-fe/eap-contracts` 引用。
2
+ * 秒钳制与 abort 识别从 `@gt-fe/eap-contracts` 来。
3
+ * 本文件保留:本次工具 execution.timeout → 毫秒。
5
4
  */
6
- export const DEFAULT_TOOL_TIMEOUT_SEC = 30;
7
- export const DEFAULT_HTTP_TOOL_TIMEOUT_SEC = 10;
8
- export const MIN_TOOL_TIMEOUT_SEC = 1;
9
- export const MAX_TOOL_TIMEOUT_SEC = 300;
10
- export function resolveToolTimeoutSec(timeoutSec, fallbackSec = DEFAULT_TOOL_TIMEOUT_SEC) {
11
- if (typeof timeoutSec !== 'number' || !Number.isFinite(timeoutSec) || timeoutSec <= 0) {
12
- return fallbackSec;
5
+ export { DEFAULT_HTTP_TOOL_TIMEOUT_SEC, DEFAULT_TOOL_TIMEOUT_SEC, MAX_TOOL_TIMEOUT_SEC, MIN_TOOL_TIMEOUT_SEC, abortOrTimeoutMessage, isAbortError, resolveToolTimeoutSec, toToolTimeoutMs, toolTimeoutErrorMessage, } from '@gt-fe/eap-contracts';
6
+ import { toToolTimeoutMs } from '@gt-fe/eap-contracts';
7
+ /** 清单 tools[].timeout / 展开后 execution.timeout(秒)→ 本次调用毫秒;没有则用装配默认。 */
8
+ export function timeoutMsFromExecution(execution, fallbackMs) {
9
+ if (!execution || typeof execution !== 'object' || Array.isArray(execution)) {
10
+ return fallbackMs;
13
11
  }
14
- return Math.min(MAX_TOOL_TIMEOUT_SEC, Math.max(MIN_TOOL_TIMEOUT_SEC, Math.trunc(timeoutSec)));
15
- }
16
- export function toToolTimeoutMs(timeoutSec, fallbackSec = DEFAULT_TOOL_TIMEOUT_SEC) {
17
- return resolveToolTimeoutSec(timeoutSec, fallbackSec) * 1000;
18
- }
19
- export function isAbortError(error) {
20
- if (!error || typeof error !== 'object')
21
- return false;
22
- const err = error;
23
- if (err.name === 'AbortError' || err.name === 'TimeoutError' || err.code === 'ABORT_ERR') {
24
- return true;
12
+ const timeout = execution.timeout;
13
+ if (typeof timeout !== 'number' || !Number.isFinite(timeout) || timeout <= 0) {
14
+ return fallbackMs;
25
15
  }
26
- const msg = (err.message ?? '').toLowerCase();
27
- return (msg === 'this operation was aborted'
28
- || msg === 'the operation was aborted'
29
- || msg.includes('this operation was aborted')
30
- || msg.includes('the operation was aborted'));
31
- }
32
- export function toolTimeoutErrorMessage(timeoutSec) {
33
- return `timeout after ${resolveToolTimeoutSec(timeoutSec)}s`;
34
- }
35
- export function abortOrTimeoutMessage(error, timeoutSec) {
36
- if (isAbortError(error))
37
- return toolTimeoutErrorMessage(timeoutSec);
38
- return error instanceof Error ? error.message : String(error);
16
+ return toToolTimeoutMs(timeout);
39
17
  }
@@ -12,6 +12,8 @@ export interface AuthContext {
12
12
  delegationToken: string;
13
13
  /** Agent 对外主键 */
14
14
  agentCode: string;
15
+ /** 本次请求的 Agent 预装版本(目录名);Gateway 写根用 */
16
+ agentVersion?: string;
15
17
  }
16
18
  /**
17
19
  * ToolCallRequest — Tool 调用请求
@@ -22,9 +24,13 @@ export interface ToolCallRequest {
22
24
  arguments: Record<string, unknown>;
23
25
  /**
24
26
  * 工具语义化版本(来自 LoadedToolDefinition.manifest.version)。
25
- * McpToolExecutor 在 Gateway `tools/call` 中透传为 params.version。
27
+ * McpToolExecutor 只放进 Gateway `params._meta.version`,不写 params 顶层。
26
28
  */
27
29
  toolVersion?: string;
30
+ /**
31
+ * MCP 工具集编码。McpToolExecutor 只放进 Gateway `params._meta.toolsetCode`。
32
+ */
33
+ toolsetCode?: string;
28
34
  authContext: AuthContext;
29
35
  dataScope?: 'user-bound' | 'department-bound' | 'global';
30
36
  resourceOwnerId?: string;
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { isBuiltinToolExecution } from '@gt-fe/eap-contracts';
8
8
  import { assertToolPermission, GovernanceUnavailableError, PermissionDeniedError, } from '../governance/permission-guard.js';
9
+ import { toModelVisibleToolContent } from './model-visible-tool-content.js';
9
10
  /**
10
11
  * ToolOrchestrator — Tool 调用编排主服务
11
12
  */
@@ -70,7 +71,11 @@ export class ToolOrchestrator {
70
71
  }
71
72
  }
72
73
  try {
73
- return await this.toolExecutor.execute(request);
74
+ const result = await this.toolExecutor.execute(request);
75
+ if (result.success && result.content) {
76
+ return { ...result, content: toModelVisibleToolContent(result.content) };
77
+ }
78
+ return result;
74
79
  }
75
80
  catch (error) {
76
81
  const message = error instanceof Error ? error.message : 'Unknown execution error';
@@ -10,6 +10,7 @@
10
10
  * @see design.md §1.2 Tool结果注入State → 回到LLM节点(ReAct循环)
11
11
  * @see requirements.md Requirement 8 AC4
12
12
  */
13
+ import { toModelVisibleToolContent } from './model-visible-tool-content.js';
13
14
  // ─────────────────────────────────────────────
14
15
  // ToolResultInjector
15
16
  // ─────────────────────────────────────────────
@@ -51,7 +52,7 @@ export class ToolResultInjector {
51
52
  */
52
53
  buildToolMessage(result) {
53
54
  const content = result.success
54
- ? result.content
55
+ ? toModelVisibleToolContent(result.content)
55
56
  : `[Tool Error] ${result.error ?? 'Unknown error'}`;
56
57
  return {
57
58
  role: 'tool',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gt-fe/eap-runtime",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
4
4
  "description": "Enterprise Agent Platform — Runtime核心:Graph执行引擎、Task生命周期、Session管理、Checkpoint持久化",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -20,7 +20,7 @@
20
20
  "ioredis": "^5.4.0",
21
21
  "pg": "^8.13.0",
22
22
  "yaml": "^2.6.1",
23
- "@gt-fe/eap-contracts": "0.0.12"
23
+ "@gt-fe/eap-contracts": "0.0.13"
24
24
  },
25
25
  "publishConfig": {
26
26
  "registry": "https://registry.npmjs.org/",