@gt-fe/eap-runtime 0.0.10 → 0.0.11
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 -3
- package/dist/api/handlers/invoke.handler.js +4 -0
- package/dist/api/handlers/stream.handler.js +5 -1
- package/dist/api/routes/agent.routes.d.ts +2 -2
- 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/contracts/trace-collector.d.ts +1 -1
- package/dist/governance/governance-client.d.ts +1 -1
- package/dist/governance/governance-client.js +40 -16
- package/dist/governance/index.d.ts +2 -2
- package/dist/governance/index.js +1 -1
- package/dist/governance/permission-guard.d.ts +2 -1
- package/dist/governance/permission-guard.js +13 -4
- package/dist/graph/adapters/langgraph.adapter.js +12 -7
- package/dist/graph/contracts/executable-graph.d.ts +8 -6
- package/dist/graph/contracts/executable-graph.js +1 -1
- package/dist/graph/hitl/hitl-handler.d.ts +3 -0
- package/dist/graph/hitl/hitl-handler.js +4 -1
- package/dist/graph/nodes/tool-node.js +27 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/providers/echo-llm-provider.js +2 -2
- package/dist/providers/local-llm-provider.d.ts +1 -1
- package/dist/providers/local-llm-provider.js +1 -1
- package/dist/providers/routing-llm-provider.js +1 -1
- 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.d.ts +14 -2
- package/dist/registry/deployment-snapshot.js +57 -31
- package/dist/registry/graph-loader.d.ts +1 -1
- package/dist/registry/graph-loader.js +4 -4
- package/dist/registry/index.d.ts +1 -1
- package/dist/registry/index.js +1 -1
- 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.js +19 -5
- package/dist/runtime/dev-runtime.d.ts +1 -1
- package/dist/runtime/dev-runtime.js +6 -10
- package/dist/runtime/types.d.ts +2 -2
- package/dist/server.js +1 -1
- package/dist/sse/events/skill-events.d.ts +1 -1
- package/dist/sse/events/skill-events.js +1 -1
- package/dist/tool/create-tool-executor.d.ts +1 -1
- package/dist/tool/dev-http-executor.d.ts +1 -1
- package/dist/tool/dev-http-executor.js +10 -7
- package/dist/tool/dev-mcp-executor.js +5 -3
- package/dist/tool/dev-tool-executor.d.ts +1 -1
- package/dist/tool/dev-tool-executor.js +2 -2
- package/dist/tool/harness-command.d.ts +32 -0
- package/dist/tool/harness-command.js +276 -0
- package/dist/tool/harness-primitives.d.ts +25 -3
- package/dist/tool/harness-primitives.js +157 -50
- package/dist/tool/http-tool-executor.js +15 -20
- package/dist/tool/index.d.ts +3 -2
- package/dist/tool/index.js +3 -2
- package/dist/tool/local-mcp-client.d.ts +1 -1
- package/dist/tool/local-mcp-client.js +54 -23
- package/dist/tool/local-tool-executor.d.ts +1 -1
- package/dist/tool/local-tool-executor.js +13 -9
- package/dist/tool/mcp-tool-executor.js +14 -6
- package/dist/tool/skill-script-refs.d.ts +20 -0
- package/dist/tool/skill-script-refs.js +62 -0
- package/dist/tool/system-prompt-builder.d.ts +2 -2
- package/dist/tool/system-prompt-builder.js +8 -5
- package/dist/tool/timeout.d.ts +14 -0
- package/dist/tool/timeout.js +36 -0
- package/dist/tool/tool-confirmation.js +2 -1
- package/dist/tool/tool-execution-router.d.ts +1 -1
- package/dist/tool/tool-execution-router.js +1 -1
- package/dist/tool/tool-executor.interface.d.ts +13 -3
- package/dist/tool/tool-orchestrator.d.ts +1 -1
- package/dist/tool/tool-orchestrator.js +6 -3
- 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 +2 -2
- package/dist/common/concurrency.d.ts +0 -79
- package/dist/common/concurrency.js +0 -121
- package/dist/common/queue-consumer.d.ts +0 -125
- package/dist/common/queue-consumer.js +0 -159
- package/dist/tool/mock-tool-executor.d.ts +0 -28
- package/dist/tool/mock-tool-executor.js +0 -65
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
import { spawn } from 'node:child_process';
|
|
8
8
|
import { existsSync } from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
|
-
import { resolveReadyInstallByToolName, resolveMcpExecutionFromInstall, resolveCodeHandlerFromInstall, } from '@
|
|
10
|
+
import { ErrorCodes, resolveReadyInstallByToolName, resolveMcpExecutionFromInstall, resolveCodeHandlerFromInstall, } from '@eap/contracts';
|
|
11
|
+
import { isAbortError, toToolTimeoutMs, toolTimeoutErrorMessage } from './timeout.js';
|
|
11
12
|
import { fileArtifactFromToolOutput, fileBasesFromEnv } from './file-artifact.js';
|
|
12
13
|
import { LocalMcpClient } from './local-mcp-client.js';
|
|
13
14
|
/**
|
|
@@ -66,7 +67,9 @@ function runHandler(handlerPath, input, timeoutMs, execution) {
|
|
|
66
67
|
};
|
|
67
68
|
const timer = setTimeout(() => {
|
|
68
69
|
child.kill('SIGKILL');
|
|
69
|
-
|
|
70
|
+
const timeoutErr = new Error(toolTimeoutErrorMessage(Math.max(1, Math.round(timeoutMs / 1000))));
|
|
71
|
+
timeoutErr.name = 'TimeoutError';
|
|
72
|
+
finish(() => reject(timeoutErr));
|
|
70
73
|
}, timeoutMs);
|
|
71
74
|
child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
72
75
|
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
@@ -112,7 +115,7 @@ export function resolveCodeExecutionFromRequest(request) {
|
|
|
112
115
|
const handler = typeof e.handler === 'string' ? e.handler : '';
|
|
113
116
|
const command = typeof e.command === 'string' ? e.command : undefined;
|
|
114
117
|
const args = Array.isArray(e.args) ? e.args.map(String) : undefined;
|
|
115
|
-
const timeoutMs = typeof e.timeout === 'number' && e.timeout > 0 ? e.timeout : undefined;
|
|
118
|
+
const timeoutMs = typeof e.timeout === 'number' && e.timeout > 0 ? toToolTimeoutMs(e.timeout) : undefined;
|
|
116
119
|
if (runtime === 'executable') {
|
|
117
120
|
const handlerPath = handler
|
|
118
121
|
? (path.isAbsolute(handler) || !bundlePath
|
|
@@ -181,14 +184,14 @@ export class LocalToolExecutor {
|
|
|
181
184
|
};
|
|
182
185
|
}
|
|
183
186
|
catch (error) {
|
|
184
|
-
const
|
|
187
|
+
const aborted = isAbortError(error);
|
|
185
188
|
return {
|
|
186
189
|
toolCallId: request.toolCallId,
|
|
187
190
|
toolName: request.toolName,
|
|
188
191
|
content: '',
|
|
189
192
|
success: false,
|
|
190
|
-
error: message,
|
|
191
|
-
errorCode: 'EXECUTION_FAILED',
|
|
193
|
+
error: error instanceof Error ? error.message : String(error),
|
|
194
|
+
errorCode: aborted ? ErrorCodes.TOOL_TIMEOUT : 'EXECUTION_FAILED',
|
|
192
195
|
runtime: 'local',
|
|
193
196
|
};
|
|
194
197
|
}
|
|
@@ -203,6 +206,7 @@ export class LocalToolExecutor {
|
|
|
203
206
|
handlerPath,
|
|
204
207
|
runtime: execution?.runtime,
|
|
205
208
|
args: execution?.args,
|
|
209
|
+
timeoutMs: typeof execution?.timeout === 'number' ? toToolTimeoutMs(execution.timeout) : undefined,
|
|
206
210
|
});
|
|
207
211
|
}
|
|
208
212
|
async executeMcp(request, install) {
|
|
@@ -232,14 +236,14 @@ export class LocalToolExecutor {
|
|
|
232
236
|
};
|
|
233
237
|
}
|
|
234
238
|
catch (error) {
|
|
235
|
-
const
|
|
239
|
+
const aborted = isAbortError(error);
|
|
236
240
|
return {
|
|
237
241
|
toolCallId: request.toolCallId,
|
|
238
242
|
toolName: request.toolName,
|
|
239
243
|
content: '',
|
|
240
244
|
success: false,
|
|
241
|
-
error: message,
|
|
242
|
-
errorCode: 'EXECUTION_FAILED',
|
|
245
|
+
error: error instanceof Error ? error.message : String(error),
|
|
246
|
+
errorCode: aborted ? ErrorCodes.TOOL_TIMEOUT : 'EXECUTION_FAILED',
|
|
243
247
|
runtime: 'local',
|
|
244
248
|
};
|
|
245
249
|
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @see docs/contracts/tool-execution.md §2.0
|
|
5
5
|
*/
|
|
6
|
+
import { ErrorCodes } from '@eap/contracts';
|
|
7
|
+
import { abortOrTimeoutMessage, isAbortError } from './timeout.js';
|
|
6
8
|
function resolveAgentCode(request) {
|
|
7
9
|
return request.authContext.agentCode;
|
|
8
10
|
}
|
|
@@ -14,9 +16,14 @@ function formatContent(result) {
|
|
|
14
16
|
}
|
|
15
17
|
function mapMcpError(status, body) {
|
|
16
18
|
const eapCode = body?.error?.data?.eapCode;
|
|
17
|
-
const message = body?.error?.message ?? `HTTP ${status}`;
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
const message = (body?.error?.message ?? '').trim() || `HTTP ${status}`;
|
|
20
|
+
// 仅 HTTP 403 视为权限拒绝。eapCode 含 auth 是 hop 认证失败,不是 C2。
|
|
21
|
+
if (status === 403) {
|
|
22
|
+
return {
|
|
23
|
+
error: message === `HTTP ${status}` ? '用户无该 Tool 执行权限' : message,
|
|
24
|
+
errorCode: eapCode ?? 'NO_PERMISSION',
|
|
25
|
+
retryable: false,
|
|
26
|
+
};
|
|
20
27
|
}
|
|
21
28
|
if (status === 429) {
|
|
22
29
|
return { error: message, errorCode: eapCode ?? 'RATE_LIMITED', retryable: true };
|
|
@@ -104,14 +111,15 @@ export class McpToolExecutor {
|
|
|
104
111
|
};
|
|
105
112
|
}
|
|
106
113
|
catch (err) {
|
|
107
|
-
const
|
|
114
|
+
const timeoutSec = Math.max(1, Math.round(this.timeout / 1000));
|
|
115
|
+
const aborted = isAbortError(err);
|
|
108
116
|
return {
|
|
109
117
|
toolCallId: request.toolCallId,
|
|
110
118
|
toolName: request.toolName,
|
|
111
119
|
content: '',
|
|
112
120
|
success: false,
|
|
113
|
-
error:
|
|
114
|
-
errorCode: 'EXECUTION_FAILED',
|
|
121
|
+
error: abortOrTimeoutMessage(err, timeoutSec),
|
|
122
|
+
errorCode: aborted ? ErrorCodes.TOOL_TIMEOUT : 'EXECUTION_FAILED',
|
|
115
123
|
retryable: false,
|
|
116
124
|
runtime: 'server',
|
|
117
125
|
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 喂给模型的 SKILL.md 正文展开(不改磁盘)。
|
|
3
|
+
* 对标 Claude `${CLAUDE_SKILL_DIR}`:作者仍写包内相对路径。
|
|
4
|
+
* 官方规范不要求脚本必须在 `scripts/`;EAP_SKILL_DIR 是整包绝对根。
|
|
5
|
+
* 展开成绝对路径,避免 Windows cmd 不认 `$VAR`、多技能时 env 未注入。
|
|
6
|
+
*/
|
|
7
|
+
export declare const EAP_SKILL_DIR_ENV = "EAP_SKILL_DIR";
|
|
8
|
+
export interface ExpandSkillRefsOptions {
|
|
9
|
+
platform?: string;
|
|
10
|
+
exists?: (absPath: string) => boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function normalizeSkillDir(skillDir: string): string;
|
|
13
|
+
export declare function skillEnvRef(platform?: string): string;
|
|
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;
|
|
20
|
+
export declare function isSkillEntryFile(filePath: string): boolean;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 喂给模型的 SKILL.md 正文展开(不改磁盘)。
|
|
3
|
+
* 对标 Claude `${CLAUDE_SKILL_DIR}`:作者仍写包内相对路径。
|
|
4
|
+
* 官方规范不要求脚本必须在 `scripts/`;EAP_SKILL_DIR 是整包绝对根。
|
|
5
|
+
* 展开成绝对路径,避免 Windows cmd 不认 `$VAR`、多技能时 env 未注入。
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
export const EAP_SKILL_DIR_ENV = 'EAP_SKILL_DIR';
|
|
10
|
+
/** 看起来像可执行/源码的扩展名。不是目录白名单。 */
|
|
11
|
+
const CODE_EXT = 'py|js|mjs|cjs|ts|tsx|jsx|sh|bash|zsh|ps1|bat|cmd|exe|rb|pl|php';
|
|
12
|
+
const RELATIVE_CODE_PATH = new RegExp(`(^|[^$\\w./\\\\:%])(\\.\\/)?((?:[\\w.-]+[\\\\/])*[\\w.-]+\\.(?:${CODE_EXT}))\\b`, 'gi');
|
|
13
|
+
export function normalizeSkillDir(skillDir) {
|
|
14
|
+
return skillDir.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
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
|
+
function defaultExists(absPath) {
|
|
25
|
+
try {
|
|
26
|
+
return existsSync(absPath);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 包内相对脚本路径 → 技能目录绝对路径。
|
|
34
|
+
* 带目录或 `./` 的(`bin/foo.py`、`./main.py`、`scripts/a.py`)一律展开。
|
|
35
|
+
* 根上的裸文件名(`main.py`)仅当包内确实有该文件才展开,以免改写「写到用户目录再跑」的例子。
|
|
36
|
+
*/
|
|
37
|
+
export function expandSkillScriptRefs(body, skillDir, platformOrOpts) {
|
|
38
|
+
const opts = asOptions(platformOrOpts);
|
|
39
|
+
const platform = opts.platform ?? process.platform;
|
|
40
|
+
const exists = opts.exists ?? defaultExists;
|
|
41
|
+
const dir = normalizeSkillDir(skillDir);
|
|
42
|
+
const expanded = body.replace(RELATIVE_CODE_PATH, (full, prefix, dotSlash, rel) => {
|
|
43
|
+
const relNorm = rel.replace(/\\/g, '/');
|
|
44
|
+
const packageRelative = Boolean(dotSlash) || relNorm.includes('/');
|
|
45
|
+
if (!packageRelative && !exists(path.join(skillDir, relNorm))) {
|
|
46
|
+
return full;
|
|
47
|
+
}
|
|
48
|
+
return `${prefix}${dir}/${relNorm}`;
|
|
49
|
+
});
|
|
50
|
+
const envRef = skillEnvRef(platform);
|
|
51
|
+
const envPath = platform === 'win32' ? `${envRef}\\<相对路径>` : `${envRef}/<相对路径>`;
|
|
52
|
+
const note = `技能目录(只读)环境变量 ${EAP_SKILL_DIR_ENV}=${dir}。` +
|
|
53
|
+
`包内任意文件用 ${envPath}(不限 scripts/)。` +
|
|
54
|
+
`读入正文里像包内脚本的相对路径已展开为绝对路径。` +
|
|
55
|
+
`当前工作目录是用户目录,相对路径写出在用户目录。\n\n`;
|
|
56
|
+
if (body.includes(`${EAP_SKILL_DIR_ENV}=`))
|
|
57
|
+
return expanded;
|
|
58
|
+
return note + expanded;
|
|
59
|
+
}
|
|
60
|
+
export function isSkillEntryFile(filePath) {
|
|
61
|
+
return /(^|[\\/])(SKILL\.md|skill\.md)$/i.test(filePath);
|
|
62
|
+
}
|
|
@@ -75,13 +75,13 @@ export interface PromptBuildContext {
|
|
|
75
75
|
agentCode?: string;
|
|
76
76
|
/** 当前调用用户标识(可选,供「我 / 我的」类指代) */
|
|
77
77
|
userId?: string | null;
|
|
78
|
-
/**
|
|
78
|
+
/** 当前用户目录(`users/<userId>`,含用户 skills) */
|
|
79
79
|
workingDirectory?: string;
|
|
80
80
|
os?: {
|
|
81
81
|
platform: string;
|
|
82
82
|
release: string;
|
|
83
83
|
};
|
|
84
|
-
/**
|
|
84
|
+
/** 允许 read_file 的根目录(写根 + 已装配 tools/skills) */
|
|
85
85
|
jailRoots?: string[];
|
|
86
86
|
skillDirs?: Array<{
|
|
87
87
|
skillId: string;
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
*
|
|
18
18
|
* @see docs/status/module-status-and-backlog.md RT-5
|
|
19
19
|
*/
|
|
20
|
-
import { isBuiltinToolExecution } from '@
|
|
20
|
+
import { isBuiltinToolExecution } from '@eap/contracts';
|
|
21
|
+
import { commandDialectLines } from './harness-command.js';
|
|
21
22
|
// ─────────────────────────────────────────────
|
|
22
23
|
// 平台规则常量(一年改不了两次,不值得搞配置表)
|
|
23
24
|
// ─────────────────────────────────────────────
|
|
@@ -192,10 +193,12 @@ export class SystemPromptBuilder {
|
|
|
192
193
|
const lines = ['## 文件与命令权限'];
|
|
193
194
|
if (workdir) {
|
|
194
195
|
lines.push(`工作目录:${workdir}`);
|
|
195
|
-
lines.push('`execute_command` 的 cwd
|
|
196
|
+
lines.push('`execute_command` 的 cwd **必须**是工作目录或其子目录,不能是预装技能/工具包。');
|
|
197
|
+
lines.push('技能包只读。跑包内文件用环境变量 `EAP_SKILL_DIR`(技能包绝对根,不限 `scripts/`)。相对写出在工作目录。');
|
|
196
198
|
}
|
|
199
|
+
lines.push(...commandDialectLines(context.os?.platform ?? process.platform));
|
|
197
200
|
if (skills.length > 0) {
|
|
198
|
-
lines.push('已装配技能目录(要用时 `read_file` 读该目录下的 `SKILL.md
|
|
201
|
+
lines.push('已装配技能目录(要用时 `read_file` 读该目录下的 `SKILL.md`;跑包内文件用 `EAP_SKILL_DIR/<相对路径>`):');
|
|
199
202
|
for (const skill of skills) {
|
|
200
203
|
const entry = skill.entryFile ?? `${skill.bundlePath}/SKILL.md`;
|
|
201
204
|
lines.push(`- ${skill.skillId}:${skill.bundlePath} (入口 ${entry})`);
|
|
@@ -208,12 +211,12 @@ export class SystemPromptBuilder {
|
|
|
208
211
|
lines.push(`- ${root}`);
|
|
209
212
|
}
|
|
210
213
|
if (workdir) {
|
|
211
|
-
lines.push('`write_file`
|
|
214
|
+
lines.push('`write_file` **仅**允许写入当前用户目录(含用户 skills);预装 tools / skills 只读。');
|
|
212
215
|
}
|
|
213
216
|
lines.push([
|
|
214
217
|
'硬限制:',
|
|
215
218
|
'- `read_file` / `write_file` 的路径必须落在上述允许根内。用户要求越界必须拒绝。',
|
|
216
|
-
'- `execute_command` 的 cwd
|
|
219
|
+
'- `execute_command` 的 cwd 必须落在工作目录内;命令语法跟当前 shell;子进程不继承 API Key 等密钥环境变量。命令本身不是操作系统级文件系统沙箱,禁止尝试读取 `/etc`、家目录或 jail 外路径。',
|
|
217
220
|
'- 禁止用 `..` 逃逸;禁止把 jail 外的绝对路径当参数。',
|
|
218
221
|
'- 不要把内部绝对路径、offload 目录原样展示给用户。',
|
|
219
222
|
'- 技能正文按需读取,不要把技能目录整包读进回复。',
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool / toolset `timeout`:秒。
|
|
3
|
+
* 与 `@eap/contracts` `src/tool/timeout.ts` 同语义;Runtime 钉的是 contracts 0.0.9,
|
|
4
|
+
* 尚未含这些导出。发版 contracts 后改为从 `@eap/contracts` 引用。
|
|
5
|
+
*/
|
|
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;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool / toolset `timeout`:秒。
|
|
3
|
+
* 与 `@eap/contracts` `src/tool/timeout.ts` 同语义;Runtime 钉的是 contracts 0.0.9,
|
|
4
|
+
* 尚未含这些导出。发版 contracts 后改为从 `@eap/contracts` 引用。
|
|
5
|
+
*/
|
|
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;
|
|
13
|
+
}
|
|
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;
|
|
25
|
+
}
|
|
26
|
+
const msg = (err.message ?? '').toLowerCase();
|
|
27
|
+
return msg === 'this operation was aborted' || msg === 'the operation was aborted';
|
|
28
|
+
}
|
|
29
|
+
export function toolTimeoutErrorMessage(timeoutSec) {
|
|
30
|
+
return `timeout after ${resolveToolTimeoutSec(timeoutSec)}s`;
|
|
31
|
+
}
|
|
32
|
+
export function abortOrTimeoutMessage(error, timeoutSec) {
|
|
33
|
+
if (isAbortError(error))
|
|
34
|
+
return toolTimeoutErrorMessage(timeoutSec);
|
|
35
|
+
return error instanceof Error ? error.message : String(error);
|
|
36
|
+
}
|
|
@@ -50,7 +50,8 @@ export class ToolConfirmationInterceptor {
|
|
|
50
50
|
toolName: request.toolName,
|
|
51
51
|
content: `用户拒绝执行工具 ${request.toolName}`,
|
|
52
52
|
success: false,
|
|
53
|
-
error:
|
|
53
|
+
error: `用户拒绝执行工具 ${request.toolName}`,
|
|
54
|
+
errorCode: 'hitl_rejected',
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
57
|
const resolvedThreadId = threadId ??
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* @see design.md §1.8.7
|
|
6
6
|
*/
|
|
7
7
|
import type { IToolExecutor, ToolCallRequest } from './tool-executor.interface.js';
|
|
8
|
-
import type { ILocalToolInstallStore } from '@
|
|
8
|
+
import type { ILocalToolInstallStore } from '@eap/contracts';
|
|
9
9
|
export type DeploymentContext = 'server' | 'desktop' | 'dev';
|
|
10
10
|
export interface ToolExecutionRouterOptions {
|
|
11
11
|
httpExecutor: IToolExecutor;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @see docs/contracts/tool-execution.md §6
|
|
5
5
|
* @see design.md §1.8.7
|
|
6
6
|
*/
|
|
7
|
-
import { resolveReadyInstallByToolName } from '@
|
|
7
|
+
import { resolveReadyInstallByToolName } from '@eap/contracts';
|
|
8
8
|
import { isLocalPreferredExecution } from './local-tool-executor.js';
|
|
9
9
|
export class ToolExecutionRouter {
|
|
10
10
|
options;
|
|
@@ -47,10 +47,20 @@ export interface ToolCallRequest {
|
|
|
47
47
|
*/
|
|
48
48
|
execution?: unknown;
|
|
49
49
|
/**
|
|
50
|
-
* Harness
|
|
51
|
-
*
|
|
50
|
+
* Harness 写根:`{agentDir}/users/<userId>`(含用户 skills)。
|
|
51
|
+
* 未传时用构造时 workdir;都缺则拒绝 write_file / execute_command。
|
|
52
|
+
*/
|
|
53
|
+
jailWorkdir?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Harness 本轮只读根:已装配 skill / toolset 的 bundlePath。
|
|
56
|
+
* 与 system prompt 的 jail 根对齐。
|
|
52
57
|
*/
|
|
53
58
|
jailExtraRoots?: string[];
|
|
59
|
+
/**
|
|
60
|
+
* 当前技能包绝对路径,注入子进程环境变量 EAP_SKILL_DIR。
|
|
61
|
+
* 对标 Claude CLAUDE_SKILL_DIR。
|
|
62
|
+
*/
|
|
63
|
+
skillDir?: string;
|
|
54
64
|
}
|
|
55
65
|
/**
|
|
56
66
|
* ToolCallResult — Tool 调用结果
|
|
@@ -64,7 +74,7 @@ export interface ToolCallResult {
|
|
|
64
74
|
/** Gateway / 本地执行结构化错误码 */
|
|
65
75
|
errorCode?: string;
|
|
66
76
|
retryable?: boolean;
|
|
67
|
-
runtime?: 'local' | 'server'
|
|
77
|
+
runtime?: 'local' | 'server';
|
|
68
78
|
/** 工具产出的可下载/可打开文件;不进模型正文 */
|
|
69
79
|
file?: {
|
|
70
80
|
fileName: string;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @see design.md §1.1 Tool调用编排
|
|
5
5
|
* @see requirements.md Requirement 8 AC1, AC3
|
|
6
6
|
*/
|
|
7
|
-
import type
|
|
7
|
+
import { type ExecutionContext } from '@eap/contracts';
|
|
8
8
|
import type { IGovernanceClient } from '../governance/governance-client.js';
|
|
9
9
|
export type { AuthContext, ToolCallRequest, ToolCallResult, LLMToolCall, LLMToolCallResponse, IToolExecutor, } from './tool-executor.interface.js';
|
|
10
10
|
import type { AuthContext, ToolCallRequest, ToolCallResult, LLMToolCallResponse, IToolExecutor } from './tool-executor.interface.js';
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* @see design.md §1.1 Tool调用编排
|
|
5
5
|
* @see requirements.md Requirement 8 AC1, AC3
|
|
6
6
|
*/
|
|
7
|
+
import { isBuiltinToolExecution } from '@eap/contracts';
|
|
7
8
|
import { assertToolPermission, GovernanceUnavailableError, PermissionDeniedError, } from '../governance/permission-guard.js';
|
|
8
9
|
/**
|
|
9
10
|
* ToolOrchestrator — Tool 调用编排主服务
|
|
@@ -36,7 +37,9 @@ export class ToolOrchestrator {
|
|
|
36
37
|
});
|
|
37
38
|
}
|
|
38
39
|
async executeSingle(request, executionContext) {
|
|
39
|
-
if (this.governanceClient &&
|
|
40
|
+
if (this.governanceClient &&
|
|
41
|
+
executionContext?.delegationToken &&
|
|
42
|
+
!isBuiltinToolExecution(request.execution)) {
|
|
40
43
|
try {
|
|
41
44
|
await assertToolPermission(this.governanceClient, executionContext, request.toolName, {
|
|
42
45
|
dataScope: request.dataScope ?? 'user-bound',
|
|
@@ -50,8 +53,8 @@ export class ToolOrchestrator {
|
|
|
50
53
|
toolName: request.toolName,
|
|
51
54
|
content: '',
|
|
52
55
|
success: false,
|
|
53
|
-
error: error.
|
|
54
|
-
errorCode: error
|
|
56
|
+
error: error.message,
|
|
57
|
+
errorCode: error.reason,
|
|
55
58
|
};
|
|
56
59
|
}
|
|
57
60
|
throw error;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Runtime HTTP 错误出口 — 对齐 @
|
|
2
|
+
* Runtime HTTP 错误出口 — 对齐 @eap/contracts ErrorCodes / AppError
|
|
3
3
|
*/
|
|
4
|
-
import { type ErrorCode, type AppError } from '@
|
|
4
|
+
import { type ErrorCode, type AppError } from '@eap/contracts';
|
|
5
5
|
import type { ServerResponse } from 'node:http';
|
|
6
6
|
/** 公共 classify;连接失败不再误标为 GOVERNANCE_UNAVAILABLE */
|
|
7
7
|
export declare function classifyRuntimeError(error: unknown): AppError;
|
package/dist/utils/http-error.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Runtime HTTP 错误出口 — 对齐 @
|
|
2
|
+
* Runtime HTTP 错误出口 — 对齐 @eap/contracts ErrorCodes / AppError
|
|
3
3
|
*/
|
|
4
|
-
import { classifyError, formatUnknownError, ErrorCodes, } from '@
|
|
4
|
+
import { classifyError, formatUnknownError, ErrorCodes, } from '@eap/contracts';
|
|
5
5
|
/** 公共 classify;连接失败不再误标为 GOVERNANCE_UNAVAILABLE */
|
|
6
6
|
export function classifyRuntimeError(error) {
|
|
7
7
|
return classifyError(error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gt-fe/eap-runtime",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"description": "Enterprise Agent Platform — Runtime核心:Graph执行引擎、Task生命周期、Session管理、Checkpoint持久化",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"start": "node dist/server.js",
|
|
24
24
|
"dev": "npx tsx src/server.ts",
|
|
25
25
|
"clean": "rm -rf dist",
|
|
26
|
+
"prepublishOnly": "npm run clean && npm run build",
|
|
26
27
|
"lint": "eslint src --ext .ts",
|
|
27
28
|
"test": "vitest run"
|
|
28
29
|
},
|
|
@@ -36,7 +37,6 @@
|
|
|
36
37
|
"license": "UNLICENSED",
|
|
37
38
|
"dependencies": {
|
|
38
39
|
"@langchain/langgraph": "^0.2.0",
|
|
39
|
-
"amqplib": "^0.10.0",
|
|
40
40
|
"ioredis": "^5.4.0",
|
|
41
41
|
"pg": "^8.13.0",
|
|
42
42
|
"yaml": "^2.6.1",
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 并发控制模块
|
|
3
|
-
*
|
|
4
|
-
* 提供两种并发限制机制:
|
|
5
|
-
* 1. ConcurrencyLimiter — 基于 amqplib channel.prefetch 的 Agent 级并发控制
|
|
6
|
-
* 通过 RabbitMQ consumer prefetch 限制单 Agent 实例同时处理的任务数
|
|
7
|
-
* 2. Semaphore — 纯内存信号量,用于本地并发控制(不依赖 RabbitMQ)
|
|
8
|
-
*
|
|
9
|
-
* @see Requirement 14 AC2: 系统通过消息消费者 prefetch 参数控制单 Agent 实例的并发上限(默认 prefetch=5)
|
|
10
|
-
*/
|
|
11
|
-
import type { Channel } from 'amqplib';
|
|
12
|
-
/** ConcurrencyLimiter 默认 prefetch 值 */
|
|
13
|
-
export declare const DEFAULT_PREFETCH = 5;
|
|
14
|
-
/**
|
|
15
|
-
* 基于 amqplib consumer prefetch 的 Agent 级并发限制器
|
|
16
|
-
*
|
|
17
|
-
* RabbitMQ 的 channel.prefetch(n) 表示该 Channel 上的消费者最多同时
|
|
18
|
-
* 持有 n 条未确认消息。超出限制的消息留在 Queue 中排队等待。
|
|
19
|
-
*
|
|
20
|
-
* 整体 QPS = Pod 数 × prefetch。
|
|
21
|
-
*/
|
|
22
|
-
export declare class ConcurrencyLimiter {
|
|
23
|
-
private readonly prefetchCount;
|
|
24
|
-
/**
|
|
25
|
-
* @param prefetchCount 单 Agent 实例最大并发任务数,默认 5
|
|
26
|
-
*/
|
|
27
|
-
constructor(prefetchCount?: number);
|
|
28
|
-
/**
|
|
29
|
-
* 配置 amqplib Channel 的 prefetch
|
|
30
|
-
*
|
|
31
|
-
* 调用后该 Channel 上的消费者最多同时处理 prefetchCount 条消息。
|
|
32
|
-
* 超出限制的消息在 RabbitMQ Queue 中排队等待。
|
|
33
|
-
*
|
|
34
|
-
* @param channel amqplib Channel 实例
|
|
35
|
-
*/
|
|
36
|
-
configure(channel: Channel): Promise<void>;
|
|
37
|
-
/**
|
|
38
|
-
* 获取配置的最大并发数
|
|
39
|
-
*/
|
|
40
|
-
getMaxConcurrency(): number;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* 内存信号量 — 用于本地并发控制
|
|
44
|
-
*
|
|
45
|
-
* 当无需依赖 RabbitMQ 时(如单进程内限流、测试场景),
|
|
46
|
-
* 使用 Semaphore 控制并发执行数。
|
|
47
|
-
*
|
|
48
|
-
* 用法:
|
|
49
|
-
* ```ts
|
|
50
|
-
* const sem = new Semaphore(3);
|
|
51
|
-
* await sem.acquire(); // 等待可用槽位
|
|
52
|
-
* try {
|
|
53
|
-
* // 执行并发受限的操作
|
|
54
|
-
* } finally {
|
|
55
|
-
* sem.release(); // 释放槽位
|
|
56
|
-
* }
|
|
57
|
-
* ```
|
|
58
|
-
*/
|
|
59
|
-
export declare class Semaphore {
|
|
60
|
-
private readonly maxSlots;
|
|
61
|
-
private currentSlots;
|
|
62
|
-
private readonly waitQueue;
|
|
63
|
-
/**
|
|
64
|
-
* @param maxConcurrency 最大并发数
|
|
65
|
-
*/
|
|
66
|
-
constructor(maxConcurrency: number);
|
|
67
|
-
/**
|
|
68
|
-
* 获取一个可用槽位。如果当前无可用槽位,则等待直到有释放。
|
|
69
|
-
*/
|
|
70
|
-
acquire(): Promise<void>;
|
|
71
|
-
/**
|
|
72
|
-
* 释放一个槽位。如果有等待者,唤醒队列中最先等待的。
|
|
73
|
-
*/
|
|
74
|
-
release(): void;
|
|
75
|
-
/**
|
|
76
|
-
* 当前可用槽位数
|
|
77
|
-
*/
|
|
78
|
-
get availableSlots(): number;
|
|
79
|
-
}
|