niceeval 0.11.1 → 0.11.3
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/INDEX.md +1 -0
- package/README.md +4 -2
- package/README.zh.md +4 -2
- package/dist/agents/types.d.ts +2 -2
- package/dist/o11y/types.d.ts +3 -3
- package/dist/report/assets/colors.d.ts +1 -1
- package/dist/report/assets/colors.js +1 -1
- package/dist/report/components/attempt-detail/AttemptSource.js +1 -2
- package/dist/report/components/attempt-detail/compute.d.ts +4 -4
- package/dist/report/components/attempt-detail/compute.js +6 -6
- package/dist/report/components/attempt-detail/faces.d.ts +1 -1
- package/dist/report/components/attempt-detail/faces.js +3 -3
- package/dist/report/components/entity-lists/AttemptList.d.ts +3 -3
- package/dist/report/components/entity-lists/AttemptList.js +3 -3
- package/dist/report/components/entity-lists/ExperimentList.js +2 -2
- package/dist/report/components/entity-lists/compute.js +1 -1
- package/dist/report/components/entity-lists/faces.js +1 -1
- package/dist/report/components/entity-lists/index.d.ts +1 -1
- package/dist/report/components/entity-lists/index.js +1 -1
- package/dist/report/components/metric-views/chart-math.d.ts +2 -2
- package/dist/report/components/metric-views/chart-math.js +2 -2
- package/dist/report/components/metric-views/compute.d.ts +2 -3
- package/dist/report/components/metric-views/compute.js +3 -4
- package/dist/report/components/shared.d.ts +1 -1
- package/dist/report/components/site-components/compute.d.ts +5 -5
- package/dist/report/components/site-components/compute.js +6 -6
- package/dist/report/components/site-components/index.d.ts +7 -7
- package/dist/report/components/site-components/index.js +7 -7
- package/dist/report/components/site-components/scope-warnings.d.ts +1 -1
- package/dist/report/components/site-components/scope-warnings.js +3 -3
- package/dist/report/components/site-components/snapshot-diagnostics.js +1 -1
- package/dist/report/components/summaries/compute.d.ts +1 -1
- package/dist/report/components/summaries/compute.js +2 -2
- package/dist/report/components/summaries/faces.js +1 -1
- package/dist/report/components/summaries/index.d.ts +1 -1
- package/dist/report/components/summaries/index.js +1 -1
- package/dist/report/index.js +1 -1
- package/dist/report/model/aggregate.d.ts +2 -2
- package/dist/report/model/aggregate.js +2 -2
- package/dist/report/model/format.d.ts +2 -2
- package/dist/report/model/format.js +2 -2
- package/dist/report/model/metrics.js +3 -3
- package/dist/report/model/types.d.ts +11 -12
- package/dist/results/locator.js +1 -1
- package/dist/results/select.d.ts +4 -4
- package/dist/results/select.js +5 -5
- package/dist/results/types.d.ts +8 -8
- package/dist/runner/types.d.ts +14 -14
- package/dist/sandbox/resolve.d.ts +1 -1
- package/dist/sandbox/types.d.ts +1 -1
- package/docs-site/zh/reference/builtin-agents.mdx +21 -0
- package/docs-site/zh/reference/define-agent.mdx +1 -1
- package/docs-site/zh/reference/report-components.mdx +156 -74
- package/docs-site/zh/tutorials/custom-reports.mdx +112 -22
- package/docs-site/zh/tutorials/sandbox-agent.mdx +3 -4
- package/docs-site/zh/tutorials/sandbox-providers.mdx +15 -15
- package/docs-site/zh/tutorials/theming.mdx +169 -0
- package/package.json +2 -1
- package/src/agents/ai-sdk.test.ts +1 -1
- package/src/agents/bub-install-spec.ts +26 -8
- package/src/agents/bub.ts +40 -15
- package/src/agents/builtin.ts +11 -1
- package/src/agents/coding-cli-versions.ts +67 -0
- package/src/agents/hermes.ts +235 -0
- package/src/agents/index.ts +4 -0
- package/src/agents/langgraph.test.ts +1 -1
- package/src/agents/manifest.ts +1 -1
- package/src/agents/openai-compat.test.ts +1 -1
- package/src/agents/openclaw.ts +103 -24
- package/src/agents/opencode.ts +183 -0
- package/src/agents/sdk-streams.test.ts +1 -1
- package/src/agents/shared.ts +13 -2
- package/src/agents/types.ts +2 -2
- package/src/context/session.test.ts +1 -1
- package/src/context/session.ts +1 -1
- package/src/o11y/cost.ts +1 -1
- package/src/o11y/parsers/bub.test.ts +1 -1
- package/src/o11y/parsers/bub.ts +1 -1
- package/src/o11y/parsers/codex.test.ts +1 -1
- package/src/o11y/parsers/codex.ts +1 -1
- package/src/o11y/parsers/hermes.ts +198 -0
- package/src/o11y/parsers/openclaw.ts +25 -3
- package/src/o11y/parsers/opencode.ts +295 -0
- package/src/o11y/types.ts +3 -3
- package/src/report/assets/colors.ts +1 -1
- package/src/report/assets/styles.css +17 -15
- package/src/report/components/attempt-detail/AttemptAssertions.tsx +1 -1
- package/src/report/components/attempt-detail/AttemptConversation.tsx +1 -1
- package/src/report/components/attempt-detail/AttemptDiagnostics.tsx +1 -1
- package/src/report/components/attempt-detail/AttemptDiff.tsx +1 -1
- package/src/report/components/attempt-detail/AttemptError.tsx +1 -1
- package/src/report/components/attempt-detail/AttemptFixPrompt.tsx +1 -1
- package/src/report/components/attempt-detail/AttemptSource.tsx +2 -3
- package/src/report/components/attempt-detail/AttemptSummary.tsx +1 -1
- package/src/report/components/attempt-detail/AttemptTimeline.tsx +1 -1
- package/src/report/components/attempt-detail/AttemptTrace.tsx +1 -1
- package/src/report/components/attempt-detail/UsageTable.tsx +1 -1
- package/src/report/components/attempt-detail/attempt-components.test.tsx +1 -1
- package/src/report/components/attempt-detail/compute.ts +6 -6
- package/src/report/components/attempt-detail/faces.ts +3 -3
- package/src/report/components/attempt-detail/index.tsx +1 -1
- package/src/report/components/entity-lists/AttemptList.tsx +3 -3
- package/src/report/components/entity-lists/ExperimentList.tsx +2 -2
- package/src/report/components/entity-lists/compute.ts +1 -1
- package/src/report/components/entity-lists/faces.ts +1 -1
- package/src/report/components/entity-lists/index.tsx +1 -1
- package/src/report/components/metric-views/DeltaTable.tsx +1 -2
- package/src/report/components/metric-views/StabilityMatrix.tsx +1 -1
- package/src/report/components/metric-views/chart-math.test.ts +1 -1
- package/src/report/components/metric-views/chart-math.ts +2 -2
- package/src/report/components/metric-views/compute.ts +3 -4
- package/src/report/components/shared.ts +1 -1
- package/src/report/components/site-components/CopyFixPrompt.tsx +1 -1
- package/src/report/components/site-components/HeroCard.tsx +1 -1
- package/src/report/components/site-components/PoweredBy.tsx +1 -1
- package/src/report/components/site-components/ScopeWarnings.tsx +1 -1
- package/src/report/components/site-components/SnapshotDiagnostics.tsx +1 -1
- package/src/report/components/site-components/TraceWaterfall.tsx +1 -1
- package/src/report/components/site-components/compute.ts +6 -6
- package/src/report/components/site-components/index.tsx +7 -7
- package/src/report/components/site-components/scope-warnings.ts +3 -3
- package/src/report/components/site-components/snapshot-diagnostics.ts +1 -1
- package/src/report/components/summaries/ScopeSummary.tsx +2 -2
- package/src/report/components/summaries/compute.ts +2 -2
- package/src/report/components/summaries/faces.ts +1 -1
- package/src/report/components/summaries/index.tsx +1 -1
- package/src/report/index.ts +1 -1
- package/src/report/model/aggregate.ts +2 -2
- package/src/report/model/format.ts +2 -2
- package/src/report/model/metrics.ts +3 -3
- package/src/report/model/types.ts +12 -13
- package/src/results/annotated-source.test.ts +1 -1
- package/src/results/attempt-evidence.test.ts +1 -1
- package/src/results/copy.ts +3 -3
- package/src/results/format.ts +3 -3
- package/src/results/host-equivalence.test.ts +6 -6
- package/src/results/index.ts +1 -1
- package/src/results/locator.test.ts +2 -2
- package/src/results/locator.ts +1 -1
- package/src/results/open.ts +2 -2
- package/src/results/publish.ts +1 -1
- package/src/results/results.test.ts +6 -323
- package/src/results/select.test.ts +438 -0
- package/src/results/select.ts +6 -6
- package/src/results/skipped-notice.ts +1 -1
- package/src/results/truncate.ts +2 -2
- package/src/results/types.ts +9 -9
- package/src/results/writer.ts +4 -4
- package/src/runner/attempt.test.ts +2 -2
- package/src/runner/attempt.ts +6 -6
- package/src/runner/eval-source.test.ts +1 -1
- package/src/runner/eval-source.ts +1 -1
- package/src/runner/reporters/artifacts.ts +1 -1
- package/src/runner/run.ts +2 -2
- package/src/runner/timing.ts +1 -1
- package/src/runner/types.ts +14 -14
- package/src/sandbox/docker-agent-image.ts +36 -0
- package/src/sandbox/e2b-agent-template.test.ts +6 -21
- package/src/sandbox/e2b-agent-template.ts +44 -11
- package/src/sandbox/index.ts +8 -0
- package/src/sandbox/official-baselines.test.ts +175 -0
- package/src/sandbox/resolve.ts +1 -1
- package/src/sandbox/types.ts +1 -1
- package/src/scoring/diff.ts +1 -1
- package/src/scoring/types.ts +1 -1
- package/src/shared/facts.ts +1 -1
- package/src/show/index.ts +1 -1
- package/src/show/render.ts +1 -1
- package/src/show/show.test.ts +3 -3
- package/src/view/app/App.tsx +3 -1
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +1 -1
- package/src/view/data.test.ts +2 -2
- package/src/view/data.ts +1 -1
- package/src/view/site-base.test.ts +51 -0
- package/src/view/site.ts +16 -0
- package/src/view/styles.css +1 -2
- package/src/view/template.html +1 -0
- package/src/view/view-report.test.ts +1 -1
package/src/agents/bub.ts
CHANGED
|
@@ -22,8 +22,9 @@ import {
|
|
|
22
22
|
BUB_CHECKPOINT_SUBDIRS,
|
|
23
23
|
BUB_INSTALL_MARKER,
|
|
24
24
|
DEFAULT_BUB_OTEL_PLUGIN,
|
|
25
|
-
|
|
25
|
+
DEFAULT_BUB_REQUIREMENT,
|
|
26
26
|
bubInstallHash,
|
|
27
|
+
bubRequirement,
|
|
27
28
|
normalizeBubPackages,
|
|
28
29
|
} from "./bub-install-spec.ts";
|
|
29
30
|
|
|
@@ -56,6 +57,19 @@ export interface BubConfig {
|
|
|
56
57
|
* 落在 `.agents/skills/<name>/`,并写一段发现指引进 AGENTS.md(bub 没有原生 Skill 加载机制)。
|
|
57
58
|
*/
|
|
58
59
|
skills?: SkillSpec[];
|
|
60
|
+
/**
|
|
61
|
+
* 装哪一版 Bub(PyPI 版本号,如 `"0.4.0"`)。省略时用 NiceEval 钉的默认版本;
|
|
62
|
+
* 永远是确定版本,不装 latest —— 被测对象的版本要能从实验配置读出来。
|
|
63
|
+
*/
|
|
64
|
+
version?: string;
|
|
65
|
+
/**
|
|
66
|
+
* OTel tape store 插件的 git 依赖(时间轨的来源)。省略时用 NiceEval 钉的默认 pin。
|
|
67
|
+
*
|
|
68
|
+
* 插件与 Bub 的 tape 协议同代:默认 pin 从 `bub.tape` 取类型,要求 Bub ≥ 0.3.10;更早的
|
|
69
|
+
* 插件 commit 按 republic 的类型校验,配 Bub ≤ 0.3.9。配错代不会安装失败,而是 span 全被拒、
|
|
70
|
+
* 时间轨静默为空 —— 所以往回钉 `version` 时必须同批钉配套的插件 commit。
|
|
71
|
+
*/
|
|
72
|
+
otelPlugin?: string;
|
|
59
73
|
/**
|
|
60
74
|
* 额外装进 bub tool 环境的 Python Package,每个 Sandbox setup 时进 `uv tool install … --with <pkg>`。
|
|
61
75
|
* 规范化后的 package 列表进安装 checkpoint key:plugin 集合不同的两个 agent 变体不会复用同一个
|
|
@@ -83,15 +97,25 @@ const UV = "$HOME/.local/bin/uv";
|
|
|
83
97
|
/** bub 的 skill 目录(`skills` 生态的「通用」目录);bub 不原生扫描它,靠 AGENTS.md 的发现指引。 */
|
|
84
98
|
const SKILL_DIR = ".agents/skills";
|
|
85
99
|
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
100
|
+
// bub 与 OTel 插件的 pin 都从 config 来(缺省用源码里的默认 pin)——不设环境变量后门:
|
|
101
|
+
// 装哪个包是配置,配置的家是代码(边界见 docs/architecture.md「配置从代码来,凭据从环境来」)。
|
|
102
|
+
//
|
|
103
|
+
// override 文件不是历史包袱:插件所在 workspace 把 `bub` 声明成 git 依赖,不覆盖的话每次
|
|
104
|
+
// 安装都会去拉 Bub 主干,版本失控。所以安装前总是先把 `bub==<version>` 写进这个文件。
|
|
91
105
|
const BUB_OVERRIDE_FILE = "/tmp/bub-override.txt";
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
106
|
+
|
|
107
|
+
/** 一个 agent 变体装的那套 bub:requirement + OTel 插件,两者都进安装指纹。 */
|
|
108
|
+
interface BubInstallPin {
|
|
109
|
+
requirement: string;
|
|
110
|
+
otelPlugin: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function resolvePin(config?: BubConfig): BubInstallPin {
|
|
114
|
+
return {
|
|
115
|
+
requirement: config?.version ? bubRequirement(config.version) : DEFAULT_BUB_REQUIREMENT,
|
|
116
|
+
otelPlugin: config?.otelPlugin ?? DEFAULT_BUB_OTEL_PLUGIN,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
95
119
|
|
|
96
120
|
// NiceEval 的预制配方与运行时安装都写到 $HOME/.local;显式使用该路径,避免 PATH 上
|
|
97
121
|
// 另一个未知版本的 bub 抢先命中。
|
|
@@ -109,8 +133,8 @@ function normalizePackages(plugins?: readonly PythonPluginSpec[]): string[] {
|
|
|
109
133
|
return normalizeBubPackages((plugins ?? []).map((plugin) => plugin.package));
|
|
110
134
|
}
|
|
111
135
|
|
|
112
|
-
function installHashOf(packages: readonly string[]): string {
|
|
113
|
-
return bubInstallHash(packages,
|
|
136
|
+
function installHashOf(packages: readonly string[], pin: BubInstallPin): string {
|
|
137
|
+
return bubInstallHash(packages, pin.requirement, pin.otelPlugin);
|
|
114
138
|
}
|
|
115
139
|
|
|
116
140
|
function diskCachePath(home: string, installHash: string): string {
|
|
@@ -129,8 +153,9 @@ async function ensureBub(
|
|
|
129
153
|
home: string,
|
|
130
154
|
log: AgentContext["log"],
|
|
131
155
|
packages: readonly string[],
|
|
156
|
+
pin: BubInstallPin,
|
|
132
157
|
): Promise<void> {
|
|
133
|
-
const installHash = installHashOf(packages);
|
|
158
|
+
const installHash = installHashOf(packages, pin);
|
|
134
159
|
const marker = `${home}/${BUB_INSTALL_MARKER}`;
|
|
135
160
|
// 只信任带完整安装规格指纹的预制环境。仅 command -v bub 无法证明版本、OTel 插件和
|
|
136
161
|
// 用户 pythonPlugins 一致;NiceEval 的 E2B Bub 配方和运行时安装都会写这个 marker。
|
|
@@ -177,13 +202,13 @@ async function ensureBub(
|
|
|
177
202
|
|
|
178
203
|
try {
|
|
179
204
|
await sb.runShell(`test -x ${UV} || (curl -LsSf https://astral.sh/uv/install.sh | sh)`);
|
|
180
|
-
await sb.runShell(`printf '%s\\n' '${
|
|
205
|
+
await sb.runShell(`printf '%s\\n' '${pin.requirement}' > ${BUB_OVERRIDE_FILE}`);
|
|
181
206
|
let last = { stdout: "", stderr: "" };
|
|
182
207
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
183
208
|
// python plugin 与 bub 同一条 uv 命令装完:分两条(先装 bub、再 --reinstall 带 --with)
|
|
184
209
|
// 会让 checkpoint 抓到的环境与 key 描述的环境错位,且第二条命令白白重装一遍 bub。
|
|
185
210
|
const install = await sb.runShell(
|
|
186
|
-
`${UV} tool install --reinstall --python 3.12 --prerelease allow 'bub' --overrides ${BUB_OVERRIDE_FILE} --with '${
|
|
211
|
+
`${UV} tool install --reinstall --python 3.12 --prerelease allow 'bub' --overrides ${BUB_OVERRIDE_FILE} --with '${pin.otelPlugin}'${withPlugins ? ` ${withPlugins}` : ""}`,
|
|
187
212
|
);
|
|
188
213
|
if (install.exitCode === 0) break;
|
|
189
214
|
last = install;
|
|
@@ -257,7 +282,7 @@ export function bubAgent(config?: BubConfig): Agent {
|
|
|
257
282
|
// 跨多个 attempt 复用同一次安装:警告归属到「触发这次安装的那个 attempt」的 log,
|
|
258
283
|
// 不追求归属到全部受益 attempt(已裁决口径)。
|
|
259
284
|
const packages = normalizePackages(config?.pythonPlugins);
|
|
260
|
-
await ensureBub(sb, home, ctx.log, packages);
|
|
285
|
+
await ensureBub(sb, home, ctx.log, packages, resolvePin(config));
|
|
261
286
|
|
|
262
287
|
if (!(await sb.fileExists(`${workspace}/AGENTS.md`))) {
|
|
263
288
|
await shared.writeFile(
|
package/src/agents/builtin.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import claudeCodeDefault from "./claude-code.ts";
|
|
2
2
|
import codexDefault from "./codex.ts";
|
|
3
3
|
import bubDefault from "./bub.ts";
|
|
4
|
+
import openCodeDefault from "./opencode.ts";
|
|
5
|
+
import hermesDefault from "./hermes.ts";
|
|
6
|
+
import openClawDefault from "./openclaw.ts";
|
|
4
7
|
import type { Agent } from "../types.ts";
|
|
5
8
|
|
|
6
|
-
export const BUILTIN_AGENTS: readonly Agent[] = [
|
|
9
|
+
export const BUILTIN_AGENTS: readonly Agent[] = [
|
|
10
|
+
claudeCodeDefault,
|
|
11
|
+
codexDefault,
|
|
12
|
+
bubDefault,
|
|
13
|
+
openCodeDefault,
|
|
14
|
+
hermesDefault,
|
|
15
|
+
openClawDefault,
|
|
16
|
+
];
|
|
@@ -1,3 +1,70 @@
|
|
|
1
1
|
/** Coding-agent CLI versions used by runtime fallback installs and official sandbox recipes. */
|
|
2
2
|
export const DEFAULT_CODEX_CLI_VERSION = "0.144.1";
|
|
3
3
|
export const DEFAULT_CLAUDE_CODE_CLI_VERSION = "2.1.207";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Bub release installed from PyPI by runtime fallback installs and official sandbox recipes.
|
|
7
|
+
*
|
|
8
|
+
* 换版本时同批核对 `bub-install-spec.ts` 的 OTel 插件 pin:插件与 Bub 的 tape 协议同代,
|
|
9
|
+
* 配错代不会安装失败,而是时间轨静默为空。
|
|
10
|
+
*/
|
|
11
|
+
export const DEFAULT_BUB_VERSION = "0.4.0";
|
|
12
|
+
|
|
13
|
+
/** OpenCode CLI(npm `opencode-ai`)默认钉的版本。 */
|
|
14
|
+
export const DEFAULT_OPENCODE_CLI_VERSION = "1.18.5";
|
|
15
|
+
|
|
16
|
+
/** Hermes Agent(PyPI `hermes-agent`)默认钉的版本。 */
|
|
17
|
+
export const DEFAULT_HERMES_CLI_VERSION = "0.19.0";
|
|
18
|
+
|
|
19
|
+
/** OpenClaw CLI(npm `openclaw`)默认钉的版本。 */
|
|
20
|
+
export const DEFAULT_OPENCLAW_CLI_VERSION = "2026.7.1-2";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 有官方公共基线制品的内置 coding agent。
|
|
24
|
+
*
|
|
25
|
+
* Docker 侧六家齐全;E2B 侧目前只发布 Claude Code / Codex / Bub
|
|
26
|
+
*(见 `E2BCodingAgent`),其余 Agent 的 E2B 模板尚未进台账。
|
|
27
|
+
*/
|
|
28
|
+
export type CodingAgentBaseline =
|
|
29
|
+
| "claude-code"
|
|
30
|
+
| "codex"
|
|
31
|
+
| "bub"
|
|
32
|
+
| "opencode"
|
|
33
|
+
| "hermes"
|
|
34
|
+
| "openclaw";
|
|
35
|
+
|
|
36
|
+
/** 每个官方基线制品里装的那个 Agent 的版本——制品版本号的版本位。 */
|
|
37
|
+
export const AGENT_BASELINE_VERSION: Record<CodingAgentBaseline, string> = {
|
|
38
|
+
"claude-code": DEFAULT_CLAUDE_CODE_CLI_VERSION,
|
|
39
|
+
codex: DEFAULT_CODEX_CLI_VERSION,
|
|
40
|
+
bub: DEFAULT_BUB_VERSION,
|
|
41
|
+
opencode: DEFAULT_OPENCODE_CLI_VERSION,
|
|
42
|
+
hermes: DEFAULT_HERMES_CLI_VERSION,
|
|
43
|
+
openclaw: DEFAULT_OPENCLAW_CLI_VERSION,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* NiceEval 基线配方自身的修订号——制品版本号的 `-r` 位。
|
|
48
|
+
*
|
|
49
|
+
* Agent 版本没变、配方变了(Node 工具契约、PATH 规范化、换 pin 的 commit、插件集合)就 +1;
|
|
50
|
+
* Agent 版本一变归 1。已发布的 tag 不可原地覆盖,配方变更必须在版本里有位置表达。
|
|
51
|
+
* 一个 Agent 的 E2B 与 Docker 制品共用这个号:一个版本号 = 一套基线配方,两侧同步重建。
|
|
52
|
+
*/
|
|
53
|
+
export const AGENT_BASELINE_RECIPE_REVISION: Record<CodingAgentBaseline, number> = {
|
|
54
|
+
"claude-code": 2,
|
|
55
|
+
codex: 2,
|
|
56
|
+
bub: 1,
|
|
57
|
+
opencode: 1,
|
|
58
|
+
hermes: 1,
|
|
59
|
+
openclaw: 1,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 官方基线制品的版本 tag:`<Agent 版本>-r<配方修订>`,如 `0.144.1-r2`。
|
|
64
|
+
*
|
|
65
|
+
* niceeval 自身的版本不参与命名——库与制品内容无关,发版不该连带重建没变的制品,
|
|
66
|
+
* 换 Agent 版本也不该等库发版。
|
|
67
|
+
*/
|
|
68
|
+
export function agentBaselineVersionTag(agent: CodingAgentBaseline): string {
|
|
69
|
+
return `${AGENT_BASELINE_VERSION[agent]}-r${AGENT_BASELINE_RECIPE_REVISION[agent]}`;
|
|
70
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { defineSandboxAgent } from "../define.ts";
|
|
2
|
+
import { requireEnv, getEnv } from "../util.ts";
|
|
3
|
+
import { shared } from "./shared.ts";
|
|
4
|
+
import {
|
|
5
|
+
appendProjectInstruction,
|
|
6
|
+
installSkills,
|
|
7
|
+
installedSkillNames,
|
|
8
|
+
skillDiscoveryInstruction,
|
|
9
|
+
} from "./skills.ts";
|
|
10
|
+
import { writeAgentSetupManifest } from "./manifest.ts";
|
|
11
|
+
import { mapGenericSpans } from "../o11y/otlp/canonical.ts";
|
|
12
|
+
import { completeCoverage } from "../scoring/coverage.ts";
|
|
13
|
+
import { parseHermesTranscript, sessionIdFromHermesOutput } from "../o11y/parsers/hermes.ts";
|
|
14
|
+
import { DEFAULT_HERMES_CLI_VERSION } from "./coding-cli-versions.ts";
|
|
15
|
+
import { shellQuote } from "../sandbox/shell.ts";
|
|
16
|
+
import type { Agent, AgentSetupManifest, EvidenceCoverage, Sandbox, SkillSpec, StreamEvent } from "../types.ts";
|
|
17
|
+
|
|
18
|
+
// Hermes Agent sandbox adapter。驱动:`hermes chat -q … --yolo`;
|
|
19
|
+
// 行为轨优先 `hermes sessions export`,不足时 sqlite 读 messages。
|
|
20
|
+
// 契约见 docs/feature/adapters/sdk/hermes/README.md。
|
|
21
|
+
//
|
|
22
|
+
// Docker 沙箱会覆盖 PATH(不含 $HOME/.local/bin),所以安装与调用一律走
|
|
23
|
+
// `$HOME/.local/bin/hermes`(同 bub 裁决),不依赖 command -v。
|
|
24
|
+
|
|
25
|
+
const SKILL_DIR = ".hermes/skills";
|
|
26
|
+
const UV = "$HOME/.local/bin/uv";
|
|
27
|
+
const HERMES = "$HOME/.local/bin/hermes";
|
|
28
|
+
|
|
29
|
+
export interface HermesConfig {
|
|
30
|
+
/** 模型 API key。省略时读 HERMES_API_KEY → OPENROUTER_API_KEY → ANTHROPIC_API_KEY。 */
|
|
31
|
+
apiKey?: string;
|
|
32
|
+
/** OpenAI 兼容端点。省略时读 HERMES_API_BASE。 */
|
|
33
|
+
baseUrl?: string;
|
|
34
|
+
/** 钉 PyPI `hermes-agent` 版本;省略时用 NiceEval 默认 pin。 */
|
|
35
|
+
version?: string;
|
|
36
|
+
/** 装进沙箱的 Skill,落在 `~/.hermes/skills/<name>/`。 */
|
|
37
|
+
skills?: SkillSpec[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolveApiKey(config?: HermesConfig): string {
|
|
41
|
+
return (
|
|
42
|
+
config?.apiKey ??
|
|
43
|
+
getEnv("HERMES_API_KEY") ??
|
|
44
|
+
getEnv("OPENROUTER_API_KEY") ??
|
|
45
|
+
requireEnv("ANTHROPIC_API_KEY")
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveBaseUrl(config?: HermesConfig): string | undefined {
|
|
50
|
+
return config?.baseUrl ?? getEnv("HERMES_API_BASE");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function hermesShell(
|
|
54
|
+
sb: Sandbox,
|
|
55
|
+
args: string[],
|
|
56
|
+
env: Record<string, string>,
|
|
57
|
+
opts?: { stream?: boolean },
|
|
58
|
+
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
|
59
|
+
// HERMES 变量本身不要 quote,以便 shell 展开 $HOME。
|
|
60
|
+
const script = `${HERMES} ${args.map(shellQuote).join(" ")}`;
|
|
61
|
+
return sb.runShell(script, { env, stream: opts?.stream });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function exportSession(sb: Sandbox, sessionId: string, env: Record<string, string>): Promise<string | undefined> {
|
|
65
|
+
const outPath = `/tmp/niceeval-hermes-${sessionId}.jsonl`;
|
|
66
|
+
const res = await hermesShell(sb, ["sessions", "export", outPath, "--session-id", sessionId], env);
|
|
67
|
+
if (res.exitCode !== 0) return undefined;
|
|
68
|
+
try {
|
|
69
|
+
return await sb.readFile(outPath);
|
|
70
|
+
} catch {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function dumpMessagesFromDb(sb: Sandbox, sessionId: string): Promise<string | undefined> {
|
|
76
|
+
// 不引入宿主 sqlite3 依赖:在沙箱里用 python 读 state.db(Hermes 自带 Python 运行时)。
|
|
77
|
+
const script = `
|
|
78
|
+
import json, sqlite3, os, sys
|
|
79
|
+
db = os.path.expanduser("~/.hermes/state.db")
|
|
80
|
+
if not os.path.exists(db):
|
|
81
|
+
sys.exit(0)
|
|
82
|
+
con = sqlite3.connect(db)
|
|
83
|
+
con.row_factory = sqlite3.Row
|
|
84
|
+
sid = sys.argv[1]
|
|
85
|
+
sess = con.execute("SELECT * FROM sessions WHERE id = ?", (sid,)).fetchone()
|
|
86
|
+
if sess:
|
|
87
|
+
print(json.dumps(dict(sess), default=str))
|
|
88
|
+
rows = con.execute(
|
|
89
|
+
"SELECT role, content, tool_call_id, tool_calls, tool_name, reasoning, reasoning_content FROM messages WHERE session_id = ? ORDER BY timestamp, id",
|
|
90
|
+
(sid,),
|
|
91
|
+
).fetchall()
|
|
92
|
+
for r in rows:
|
|
93
|
+
print(json.dumps(dict(r), default=str))
|
|
94
|
+
`;
|
|
95
|
+
const res = await sb.runShell(`python3 -c ${shellQuote(script)} ${shellQuote(sessionId)}`);
|
|
96
|
+
if (res.exitCode !== 0 || !res.stdout.trim()) return undefined;
|
|
97
|
+
return res.stdout;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Hermes Agent 的内置 sandbox Agent 工厂。
|
|
102
|
+
*/
|
|
103
|
+
export function hermesAgent(config?: HermesConfig): Agent {
|
|
104
|
+
const version = config?.version ?? DEFAULT_HERMES_CLI_VERSION;
|
|
105
|
+
|
|
106
|
+
return defineSandboxAgent({
|
|
107
|
+
name: "hermes",
|
|
108
|
+
coverage: completeCoverage,
|
|
109
|
+
spanMapper: mapGenericSpans,
|
|
110
|
+
|
|
111
|
+
async setup(sb) {
|
|
112
|
+
// 装到当前沙箱用户 $HOME/.local(Docker 下是 /home/node);显式路径,不靠 PATH。
|
|
113
|
+
await sb.runShell(`test -x ${UV} || (curl -LsSf https://astral.sh/uv/install.sh | sh)`);
|
|
114
|
+
await sb.runShell(
|
|
115
|
+
`test -x ${HERMES} || ${UV} tool install hermes-agent==${version}`,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const baseUrl = resolveBaseUrl(config);
|
|
119
|
+
if (baseUrl) {
|
|
120
|
+
// OpenAI 兼容网关:写 custom provider + model.base_url;
|
|
121
|
+
// secret 进 ~/.hermes/.env,不进 git 可见配置。
|
|
122
|
+
const apiKey = resolveApiKey(config);
|
|
123
|
+
const hermesConfig = [
|
|
124
|
+
"model:",
|
|
125
|
+
" provider: custom",
|
|
126
|
+
` base_url: ${baseUrl}`,
|
|
127
|
+
"custom_providers:",
|
|
128
|
+
" - name: compat",
|
|
129
|
+
` base_url: ${baseUrl}`,
|
|
130
|
+
` api_key: ${apiKey}`,
|
|
131
|
+
"",
|
|
132
|
+
].join("\n");
|
|
133
|
+
const hermesEnv = [
|
|
134
|
+
`OPENAI_API_KEY=${apiKey}`,
|
|
135
|
+
`OPENAI_BASE_URL=${baseUrl}`,
|
|
136
|
+
"",
|
|
137
|
+
].join("\n");
|
|
138
|
+
await shared.writeFile(sb, "~/.hermes/config.yaml", hermesConfig);
|
|
139
|
+
await shared.writeFile(sb, "~/.hermes/.env", hermesEnv);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const manifest: AgentSetupManifest = { skills: [] };
|
|
143
|
+
if (config?.skills?.length) {
|
|
144
|
+
manifest.skills = await installSkills(sb, config.skills, { dir: SKILL_DIR });
|
|
145
|
+
await appendProjectInstruction(
|
|
146
|
+
sb,
|
|
147
|
+
skillDiscoveryInstruction(SKILL_DIR, installedSkillNames(manifest.skills)),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
if (manifest.skills.length) {
|
|
151
|
+
await writeAgentSetupManifest(sb, manifest);
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
async send(input, ctx) {
|
|
156
|
+
const sb = ctx.sandbox;
|
|
157
|
+
const baseUrl = resolveBaseUrl(config);
|
|
158
|
+
const args = ["chat", "-q", input.text, "--yolo", "-Q"];
|
|
159
|
+
if (ctx.model) args.push("--model", ctx.model);
|
|
160
|
+
if (baseUrl) args.push("--provider", "custom");
|
|
161
|
+
if (ctx.session.id) args.push("--resume", ctx.session.id);
|
|
162
|
+
|
|
163
|
+
const apiKey = resolveApiKey(config);
|
|
164
|
+
const env: Record<string, string> = {
|
|
165
|
+
HERMES_API_KEY: apiKey,
|
|
166
|
+
ANTHROPIC_API_KEY: apiKey,
|
|
167
|
+
OPENROUTER_API_KEY: apiKey,
|
|
168
|
+
OPENAI_API_KEY: apiKey,
|
|
169
|
+
HERMES_YOLO_MODE: "1",
|
|
170
|
+
...ctx.telemetry?.env,
|
|
171
|
+
};
|
|
172
|
+
if (baseUrl) {
|
|
173
|
+
env.HERMES_API_BASE = baseUrl;
|
|
174
|
+
env.OPENAI_BASE_URL = baseUrl;
|
|
175
|
+
env.ANTHROPIC_BASE_URL = baseUrl;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const res = await hermesShell(sb, args, env, { stream: true });
|
|
179
|
+
let sessionId =
|
|
180
|
+
sessionIdFromHermesOutput(res.stdout) ??
|
|
181
|
+
sessionIdFromHermesOutput(res.stderr) ??
|
|
182
|
+
ctx.session.id;
|
|
183
|
+
|
|
184
|
+
// 没从输出抠到 id 时,取 state.db 里最新 cli session
|
|
185
|
+
if (!sessionId) {
|
|
186
|
+
const latest = await sb.runShell(
|
|
187
|
+
`python3 -c 'import sqlite3,os;db=os.path.expanduser("~/.hermes/state.db");
|
|
188
|
+
import sys
|
|
189
|
+
if not os.path.exists(db): sys.exit(0)
|
|
190
|
+
c=sqlite3.connect(db)
|
|
191
|
+
r=c.execute("select id from sessions where source=\\"cli\\" order by started_at desc limit 1").fetchone()
|
|
192
|
+
print(r[0] if r else "")'`,
|
|
193
|
+
);
|
|
194
|
+
const id = latest.stdout.trim();
|
|
195
|
+
if (id) sessionId = id;
|
|
196
|
+
}
|
|
197
|
+
if (sessionId) ctx.session.capture(sessionId);
|
|
198
|
+
|
|
199
|
+
let raw: string | undefined;
|
|
200
|
+
if (sessionId) {
|
|
201
|
+
raw = await exportSession(sb, sessionId, env);
|
|
202
|
+
if (!raw) raw = await dumpMessagesFromDb(sb, sessionId);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const parsed = parseHermesTranscript(raw);
|
|
206
|
+
const events: StreamEvent[] = [...parsed.events];
|
|
207
|
+
|
|
208
|
+
let turnCoverage: EvidenceCoverage | undefined;
|
|
209
|
+
if (!raw || parsed.events.length === 0) {
|
|
210
|
+
const reason = "hermes session export/state.db unavailable; tool trajectory missing";
|
|
211
|
+
turnCoverage = {
|
|
212
|
+
events: { status: "unavailable", reason },
|
|
213
|
+
actions: { status: "unavailable", reason },
|
|
214
|
+
usage: { status: "unavailable", reason },
|
|
215
|
+
};
|
|
216
|
+
ctx.log("hermes transcript unavailable: negative assertions are unreliable");
|
|
217
|
+
const text = res.stdout.trim().split("\n").filter((l) => !l.startsWith("{")).slice(-8).join("\n");
|
|
218
|
+
if (text) events.push({ type: "message", role: "assistant", text });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (res.exitCode !== 0) {
|
|
222
|
+
events.push({ type: "error", message: shared.diagnoseFailure(res, parsed.events, raw) });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
events,
|
|
227
|
+
usage: parsed.usage,
|
|
228
|
+
status: res.exitCode === 0 ? "completed" : "failed",
|
|
229
|
+
...(turnCoverage ? { coverage: turnCoverage } : {}),
|
|
230
|
+
};
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export default hermesAgent();
|
package/src/agents/index.ts
CHANGED
|
@@ -88,10 +88,14 @@ export { BUILTIN_AGENTS } from "./builtin.ts";
|
|
|
88
88
|
export { claudeCodeAgent } from "./claude-code.ts";
|
|
89
89
|
export { codexAgent } from "./codex.ts";
|
|
90
90
|
export { bubAgent } from "./bub.ts";
|
|
91
|
+
export { openCodeAgent } from "./opencode.ts";
|
|
92
|
+
export { hermesAgent } from "./hermes.ts";
|
|
91
93
|
export { openClawAgent } from "./openclaw.ts";
|
|
92
94
|
export type { ClaudeCodeConfig, ClaudeCodePluginSpec } from "./claude-code.ts";
|
|
93
95
|
export type { CodexConfig, CodexPluginSpec } from "./codex.ts";
|
|
94
96
|
export type { BubConfig, PythonPluginSpec } from "./bub.ts";
|
|
97
|
+
export type { OpenCodeConfig } from "./opencode.ts";
|
|
98
|
+
export type { HermesConfig } from "./hermes.ts";
|
|
95
99
|
export type { OpenClawConfig } from "./openclaw.ts";
|
|
96
100
|
|
|
97
101
|
// 安装 manifest 的落点:adapter 写(shared.writeAgentSetup),运行器读并抬成 attempt artifact。
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// cases: docs/engineering/testing/unit/
|
|
1
|
+
// cases: docs/engineering/testing/unit/record.md
|
|
2
2
|
// 「Usage、facts 与失败命令证据落盘」桶恒互斥归一:LangChain usage_metadata 的 input_tokens
|
|
3
3
|
// 是含缓存读写的输入总量,落桶前扣掉 input_token_details 的 cache_read / cache_creation。
|
|
4
4
|
// bug: memory/estimatecost-openai-inclusive-cache-double-billed.md
|
package/src/agents/manifest.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// 安装 Manifest 的落点与读写(定稿见 docs/feature/adapters/architecture/coding-agent-extensions.md
|
|
2
|
-
// 「安装 Manifest」、docs/feature/
|
|
2
|
+
// 「安装 Manifest」、docs/feature/record/architecture.md「agent-setup.json」)。
|
|
3
3
|
//
|
|
4
4
|
// 分工:**adapter 写**(setup 收尾,知道自己装了什么)、**运行器读**(把它抬成 attempt artifact)。
|
|
5
5
|
// 中间那层只是一个双方都认的沙箱路径 —— core 不解释 manifest 内容,也不按 agent 名字分支;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// cases: docs/engineering/testing/unit/
|
|
1
|
+
// cases: docs/engineering/testing/unit/record.md
|
|
2
2
|
// 「Usage、facts 与失败命令证据落盘」桶恒互斥归一:Chat Completions / Responses 两种形状的
|
|
3
3
|
// cached_tokens 都是输入总量的子集,落 inputTokens 前扣掉;缺 cached 明细时总量原样保留。
|
|
4
4
|
// bug: memory/estimatecost-openai-inclusive-cache-double-billed.md
|
package/src/agents/openclaw.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineSandboxAgent } from "../define.ts";
|
|
2
|
-
import { requireEnv } from "../util.ts";
|
|
2
|
+
import { requireEnv, getEnv } from "../util.ts";
|
|
3
3
|
import { shared } from "./shared.ts";
|
|
4
4
|
import {
|
|
5
5
|
appendProjectInstruction,
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
import { writeAgentSetupManifest } from "./manifest.ts";
|
|
11
11
|
import { mapGenericSpans } from "../o11y/otlp/canonical.ts";
|
|
12
12
|
import { parseOpenClawTranscript, parseOpenClawRunJson } from "../o11y/parsers/openclaw.ts";
|
|
13
|
+
import { completeCoverage } from "../scoring/coverage.ts";
|
|
14
|
+
import { DEFAULT_OPENCLAW_CLI_VERSION } from "./coding-cli-versions.ts";
|
|
13
15
|
import { randomUUID } from "node:crypto";
|
|
14
16
|
import type { Agent, AgentSetupManifest, EvidenceCoverage, SkillSpec, StreamEvent } from "../types.ts";
|
|
15
17
|
|
|
@@ -25,16 +27,16 @@ import type { Agent, AgentSetupManifest, EvidenceCoverage, SkillSpec, StreamEven
|
|
|
25
27
|
// 经 ctx.log 明确记录这个限制。
|
|
26
28
|
// ───────────────────────────────────────────────────────────────────────────
|
|
27
29
|
|
|
28
|
-
// 契约要求实现前用真实 CLI fixture 钉版本;fixture 固定前先跟 npm latest 走,
|
|
29
|
-
// 钉死后归口 coding-cli-versions.ts(与 codex / claude-code 同源管理)。
|
|
30
|
-
const DEFAULT_OPENCLAW_CLI_VERSION = "latest";
|
|
31
|
-
|
|
32
30
|
/** OpenClaw 的 skill 目录(`skills` 生态的「通用」目录);发现靠 AGENTS.md 指引,不依赖原生扫描。 */
|
|
33
31
|
const SKILL_DIR = ".agents/skills";
|
|
32
|
+
/** 自定义 OpenAI 兼容网关在 openclaw.json 里的 provider id。 */
|
|
33
|
+
const COMPAT_PROVIDER = "compat";
|
|
34
34
|
|
|
35
35
|
export interface OpenClawConfig {
|
|
36
|
-
/** 模型 API key
|
|
36
|
+
/** 模型 API key。省略时读 OPENCLAW_API_KEY,再回落 ANTHROPIC_API_KEY。 */
|
|
37
37
|
apiKey?: string;
|
|
38
|
+
/** OpenAI 兼容端点。省略时读 OPENCLAW_BASE_URL。 */
|
|
39
|
+
baseUrl?: string;
|
|
38
40
|
/** 固定安装的 openclaw npm 版本(如 "1.2.3");省略时用内置默认。 */
|
|
39
41
|
version?: string;
|
|
40
42
|
/**
|
|
@@ -44,6 +46,21 @@ export interface OpenClawConfig {
|
|
|
44
46
|
skills?: SkillSpec[];
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
function resolveApiKey(config?: OpenClawConfig): string {
|
|
50
|
+
return config?.apiKey ?? getEnv("OPENCLAW_API_KEY") ?? requireEnv("ANTHROPIC_API_KEY");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveBaseUrl(config?: OpenClawConfig): string | undefined {
|
|
54
|
+
return config?.baseUrl ?? getEnv("OPENCLAW_BASE_URL");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** experiment.model → `--model`。裸模型名在自定义网关下补成 `compat/<model>`。 */
|
|
58
|
+
function resolveModelFlag(model: string | undefined, hasCompatBase: boolean): string | undefined {
|
|
59
|
+
if (!model) return undefined;
|
|
60
|
+
if (model.includes("/")) return model;
|
|
61
|
+
return hasCompatBase ? `${COMPAT_PROVIDER}/${model}` : model;
|
|
62
|
+
}
|
|
63
|
+
|
|
47
64
|
/**
|
|
48
65
|
* OpenClaw 的内置 sandbox Agent 工厂。复用 `defineSandboxAgent`、`shared` 安装工具、
|
|
49
66
|
* `ctx.session` 存取器与 canonical OTel 通用 mapper(`mapGenericSpans`);OpenClaw 方言
|
|
@@ -54,23 +71,13 @@ export interface OpenClawConfig {
|
|
|
54
71
|
* `t.newSession()` 后的新会话线自然拿到新 id,session 之间互相隔离。
|
|
55
72
|
*/
|
|
56
73
|
export function openClawAgent(config?: OpenClawConfig): Agent {
|
|
57
|
-
const getApiKey = () => config?.apiKey ?? requireEnv("ANTHROPIC_API_KEY");
|
|
58
74
|
const version = config?.version ?? DEFAULT_OPENCLAW_CLI_VERSION;
|
|
59
75
|
|
|
60
|
-
// 契约(sdk/openclaw/README.md):只有 fixture 证明完整的行为才进入公开能力。transcript
|
|
61
|
-
// 完整性尚未经真实 CLI fixture 验证,常态覆盖只声明 partial(不是 complete)——负断言在
|
|
62
|
-
// 非 complete 通道上一律 unavailable,「明确限制负断言」由覆盖声明落实,不靠口头承诺。
|
|
63
|
-
const FIXTURE_UNVERIFIED = "OpenClaw transcript completeness is not yet fixture-verified";
|
|
64
|
-
const defaultCoverage: EvidenceCoverage = {
|
|
65
|
-
events: { status: "partial", reason: FIXTURE_UNVERIFIED },
|
|
66
|
-
actions: { status: "partial", reason: FIXTURE_UNVERIFIED },
|
|
67
|
-
messages: { status: "partial", reason: FIXTURE_UNVERIFIED },
|
|
68
|
-
usage: { status: "partial", reason: FIXTURE_UNVERIFIED },
|
|
69
|
-
};
|
|
70
|
-
|
|
71
76
|
return defineSandboxAgent({
|
|
72
77
|
name: "openclaw",
|
|
73
|
-
|
|
78
|
+
// e2e 已用真实 CLI 证明 session transcript 能给出工具轨 / 消息 / 用量;
|
|
79
|
+
// 采集失败的单轮仍会在 send 里把 coverage 降成 unavailable/partial。
|
|
80
|
+
coverage: completeCoverage,
|
|
74
81
|
// OpenClaw 没有专属 span 方言 mapper:原生 span 走 canonical 通用 heuristic。
|
|
75
82
|
// OTel 内容采集关闭时只影响 trace 证据面;行为轨(下面的 transcript 解析)不受影响。
|
|
76
83
|
spanMapper: mapGenericSpans,
|
|
@@ -90,6 +97,53 @@ export function openClawAgent(config?: OpenClawConfig): Agent {
|
|
|
90
97
|
`command -v openclaw >/dev/null 2>&1 || npm install -g openclaw@${version}`,
|
|
91
98
|
);
|
|
92
99
|
|
|
100
|
+
const baseUrl = resolveBaseUrl(config);
|
|
101
|
+
if (baseUrl) {
|
|
102
|
+
// OpenAI 兼容网关走自定义 provider + openai-completions;
|
|
103
|
+
// skipBootstrap 跳过首轮身份仪式,否则 agent 会先问 "Who am I"。
|
|
104
|
+
// workspace 钉沙箱工作目录,文件工具写到 eval 可见的路径。
|
|
105
|
+
const openclawConfig = {
|
|
106
|
+
agents: {
|
|
107
|
+
defaults: {
|
|
108
|
+
skipBootstrap: true,
|
|
109
|
+
workspace: sb.workdir,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
models: {
|
|
113
|
+
mode: "merge",
|
|
114
|
+
providers: {
|
|
115
|
+
[COMPAT_PROVIDER]: {
|
|
116
|
+
baseUrl,
|
|
117
|
+
apiKey: resolveApiKey(config),
|
|
118
|
+
api: "openai-completions",
|
|
119
|
+
models: [
|
|
120
|
+
{ id: "gpt-5.6-luna", name: "gpt-5.6-luna" },
|
|
121
|
+
{ id: "gpt-5.4-mini", name: "gpt-5.4-mini" },
|
|
122
|
+
{ id: "gpt-5.4", name: "gpt-5.4" },
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
await shared.writeFile(sb, "~/.openclaw/openclaw.json", JSON.stringify(openclawConfig, null, 2));
|
|
129
|
+
// 预置最小身份文件,避免缺 IDENTITY 时仍触发仪式文案。
|
|
130
|
+
await shared.writeFile(
|
|
131
|
+
sb,
|
|
132
|
+
`${sb.workdir}/IDENTITY.md`,
|
|
133
|
+
"# Identity\n\nName: niceeval\nEmoji: ✓\n",
|
|
134
|
+
);
|
|
135
|
+
await shared.writeFile(
|
|
136
|
+
sb,
|
|
137
|
+
`${sb.workdir}/USER.md`,
|
|
138
|
+
"# User\n\nEval harness operator.\n",
|
|
139
|
+
);
|
|
140
|
+
await shared.writeFile(
|
|
141
|
+
sb,
|
|
142
|
+
`${sb.workdir}/SOUL.md`,
|
|
143
|
+
"# Soul\n\nExecute the user's task directly. Do not ask who you are.\n",
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
93
147
|
const manifest: AgentSetupManifest = { skills: [] };
|
|
94
148
|
if (config?.skills?.length) {
|
|
95
149
|
manifest.skills = await installSkills(sb, config.skills, { dir: SKILL_DIR });
|
|
@@ -110,11 +164,23 @@ export function openClawAgent(config?: OpenClawConfig): Agent {
|
|
|
110
164
|
const sessionId = ctx.session.id ?? `niceeval-${sb.sandboxId}-${randomUUID().slice(0, 8)}`;
|
|
111
165
|
ctx.session.capture(sessionId);
|
|
112
166
|
|
|
167
|
+
const baseUrl = resolveBaseUrl(config);
|
|
113
168
|
const args = ["agent", "--local", "--session-id", sessionId, "--message", input.text, "--json"];
|
|
169
|
+
const model = resolveModelFlag(ctx.model, Boolean(baseUrl));
|
|
170
|
+
if (model) args.push("--model", model);
|
|
171
|
+
|
|
172
|
+
const apiKey = resolveApiKey(config);
|
|
114
173
|
const env: Record<string, string> = {
|
|
115
|
-
|
|
174
|
+
OPENCLAW_API_KEY: apiKey,
|
|
175
|
+
ANTHROPIC_API_KEY: apiKey,
|
|
176
|
+
OPENAI_API_KEY: apiKey,
|
|
116
177
|
...ctx.telemetry?.env,
|
|
117
178
|
};
|
|
179
|
+
if (baseUrl) {
|
|
180
|
+
env.OPENCLAW_BASE_URL = baseUrl;
|
|
181
|
+
env.OPENAI_BASE_URL = baseUrl;
|
|
182
|
+
}
|
|
183
|
+
|
|
118
184
|
const res = await sb.runCommand("openclaw", args, { env, stream: true });
|
|
119
185
|
|
|
120
186
|
const runJson = parseOpenClawRunJson(res.stdout);
|
|
@@ -122,10 +188,23 @@ export function openClawAgent(config?: OpenClawConfig): Agent {
|
|
|
122
188
|
// 首轮已用自发 id 落地时不覆盖)。
|
|
123
189
|
ctx.session.capture(runJson.sessionId);
|
|
124
190
|
|
|
125
|
-
// 完整工具轨迹的唯一来源:session transcript
|
|
126
|
-
// (
|
|
127
|
-
//
|
|
128
|
-
|
|
191
|
+
// 完整工具轨迹的唯一来源:session transcript。
|
|
192
|
+
// 优先读封包给出的 sessionFile(精确);否则在 agents 目录取最新 *.jsonl,
|
|
193
|
+
// 但排除同目录旁路产物 *.trajectory.jsonl(mtime 常更新、却不是消息轨——
|
|
194
|
+
// 误读会导致 events 为空、整轮 coverage 降成 unavailable)。
|
|
195
|
+
let raw: string | undefined;
|
|
196
|
+
if (runJson.sessionFile) {
|
|
197
|
+
try {
|
|
198
|
+
raw = await sb.readFile(runJson.sessionFile);
|
|
199
|
+
} catch {
|
|
200
|
+
raw = undefined;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (raw === undefined) {
|
|
204
|
+
raw = await shared.captureLatestJsonl(sb, "~/.openclaw/agents", {
|
|
205
|
+
excludeName: /\.trajectory\.jsonl$/i,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
129
208
|
const parsed = parseOpenClawTranscript(raw);
|
|
130
209
|
const events: StreamEvent[] = [...parsed.events];
|
|
131
210
|
|