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.
Files changed (179) hide show
  1. package/INDEX.md +1 -0
  2. package/README.md +4 -2
  3. package/README.zh.md +4 -2
  4. package/dist/agents/types.d.ts +2 -2
  5. package/dist/o11y/types.d.ts +3 -3
  6. package/dist/report/assets/colors.d.ts +1 -1
  7. package/dist/report/assets/colors.js +1 -1
  8. package/dist/report/components/attempt-detail/AttemptSource.js +1 -2
  9. package/dist/report/components/attempt-detail/compute.d.ts +4 -4
  10. package/dist/report/components/attempt-detail/compute.js +6 -6
  11. package/dist/report/components/attempt-detail/faces.d.ts +1 -1
  12. package/dist/report/components/attempt-detail/faces.js +3 -3
  13. package/dist/report/components/entity-lists/AttemptList.d.ts +3 -3
  14. package/dist/report/components/entity-lists/AttemptList.js +3 -3
  15. package/dist/report/components/entity-lists/ExperimentList.js +2 -2
  16. package/dist/report/components/entity-lists/compute.js +1 -1
  17. package/dist/report/components/entity-lists/faces.js +1 -1
  18. package/dist/report/components/entity-lists/index.d.ts +1 -1
  19. package/dist/report/components/entity-lists/index.js +1 -1
  20. package/dist/report/components/metric-views/chart-math.d.ts +2 -2
  21. package/dist/report/components/metric-views/chart-math.js +2 -2
  22. package/dist/report/components/metric-views/compute.d.ts +2 -3
  23. package/dist/report/components/metric-views/compute.js +3 -4
  24. package/dist/report/components/shared.d.ts +1 -1
  25. package/dist/report/components/site-components/compute.d.ts +5 -5
  26. package/dist/report/components/site-components/compute.js +6 -6
  27. package/dist/report/components/site-components/index.d.ts +7 -7
  28. package/dist/report/components/site-components/index.js +7 -7
  29. package/dist/report/components/site-components/scope-warnings.d.ts +1 -1
  30. package/dist/report/components/site-components/scope-warnings.js +3 -3
  31. package/dist/report/components/site-components/snapshot-diagnostics.js +1 -1
  32. package/dist/report/components/summaries/compute.d.ts +1 -1
  33. package/dist/report/components/summaries/compute.js +2 -2
  34. package/dist/report/components/summaries/faces.js +1 -1
  35. package/dist/report/components/summaries/index.d.ts +1 -1
  36. package/dist/report/components/summaries/index.js +1 -1
  37. package/dist/report/index.js +1 -1
  38. package/dist/report/model/aggregate.d.ts +2 -2
  39. package/dist/report/model/aggregate.js +2 -2
  40. package/dist/report/model/format.d.ts +2 -2
  41. package/dist/report/model/format.js +2 -2
  42. package/dist/report/model/metrics.js +3 -3
  43. package/dist/report/model/types.d.ts +11 -12
  44. package/dist/results/locator.js +1 -1
  45. package/dist/results/select.d.ts +4 -4
  46. package/dist/results/select.js +5 -5
  47. package/dist/results/types.d.ts +8 -8
  48. package/dist/runner/types.d.ts +14 -14
  49. package/dist/sandbox/resolve.d.ts +1 -1
  50. package/dist/sandbox/types.d.ts +1 -1
  51. package/docs-site/zh/reference/builtin-agents.mdx +21 -0
  52. package/docs-site/zh/reference/define-agent.mdx +1 -1
  53. package/docs-site/zh/reference/report-components.mdx +156 -74
  54. package/docs-site/zh/tutorials/custom-reports.mdx +112 -22
  55. package/docs-site/zh/tutorials/sandbox-agent.mdx +3 -4
  56. package/docs-site/zh/tutorials/sandbox-providers.mdx +15 -15
  57. package/docs-site/zh/tutorials/theming.mdx +169 -0
  58. package/package.json +2 -1
  59. package/src/agents/ai-sdk.test.ts +1 -1
  60. package/src/agents/bub-install-spec.ts +26 -8
  61. package/src/agents/bub.ts +40 -15
  62. package/src/agents/builtin.ts +11 -1
  63. package/src/agents/coding-cli-versions.ts +67 -0
  64. package/src/agents/hermes.ts +235 -0
  65. package/src/agents/index.ts +4 -0
  66. package/src/agents/langgraph.test.ts +1 -1
  67. package/src/agents/manifest.ts +1 -1
  68. package/src/agents/openai-compat.test.ts +1 -1
  69. package/src/agents/openclaw.ts +103 -24
  70. package/src/agents/opencode.ts +183 -0
  71. package/src/agents/sdk-streams.test.ts +1 -1
  72. package/src/agents/shared.ts +13 -2
  73. package/src/agents/types.ts +2 -2
  74. package/src/context/session.test.ts +1 -1
  75. package/src/context/session.ts +1 -1
  76. package/src/o11y/cost.ts +1 -1
  77. package/src/o11y/parsers/bub.test.ts +1 -1
  78. package/src/o11y/parsers/bub.ts +1 -1
  79. package/src/o11y/parsers/codex.test.ts +1 -1
  80. package/src/o11y/parsers/codex.ts +1 -1
  81. package/src/o11y/parsers/hermes.ts +198 -0
  82. package/src/o11y/parsers/openclaw.ts +25 -3
  83. package/src/o11y/parsers/opencode.ts +295 -0
  84. package/src/o11y/types.ts +3 -3
  85. package/src/report/assets/colors.ts +1 -1
  86. package/src/report/assets/styles.css +17 -15
  87. package/src/report/components/attempt-detail/AttemptAssertions.tsx +1 -1
  88. package/src/report/components/attempt-detail/AttemptConversation.tsx +1 -1
  89. package/src/report/components/attempt-detail/AttemptDiagnostics.tsx +1 -1
  90. package/src/report/components/attempt-detail/AttemptDiff.tsx +1 -1
  91. package/src/report/components/attempt-detail/AttemptError.tsx +1 -1
  92. package/src/report/components/attempt-detail/AttemptFixPrompt.tsx +1 -1
  93. package/src/report/components/attempt-detail/AttemptSource.tsx +2 -3
  94. package/src/report/components/attempt-detail/AttemptSummary.tsx +1 -1
  95. package/src/report/components/attempt-detail/AttemptTimeline.tsx +1 -1
  96. package/src/report/components/attempt-detail/AttemptTrace.tsx +1 -1
  97. package/src/report/components/attempt-detail/UsageTable.tsx +1 -1
  98. package/src/report/components/attempt-detail/attempt-components.test.tsx +1 -1
  99. package/src/report/components/attempt-detail/compute.ts +6 -6
  100. package/src/report/components/attempt-detail/faces.ts +3 -3
  101. package/src/report/components/attempt-detail/index.tsx +1 -1
  102. package/src/report/components/entity-lists/AttemptList.tsx +3 -3
  103. package/src/report/components/entity-lists/ExperimentList.tsx +2 -2
  104. package/src/report/components/entity-lists/compute.ts +1 -1
  105. package/src/report/components/entity-lists/faces.ts +1 -1
  106. package/src/report/components/entity-lists/index.tsx +1 -1
  107. package/src/report/components/metric-views/DeltaTable.tsx +1 -2
  108. package/src/report/components/metric-views/StabilityMatrix.tsx +1 -1
  109. package/src/report/components/metric-views/chart-math.test.ts +1 -1
  110. package/src/report/components/metric-views/chart-math.ts +2 -2
  111. package/src/report/components/metric-views/compute.ts +3 -4
  112. package/src/report/components/shared.ts +1 -1
  113. package/src/report/components/site-components/CopyFixPrompt.tsx +1 -1
  114. package/src/report/components/site-components/HeroCard.tsx +1 -1
  115. package/src/report/components/site-components/PoweredBy.tsx +1 -1
  116. package/src/report/components/site-components/ScopeWarnings.tsx +1 -1
  117. package/src/report/components/site-components/SnapshotDiagnostics.tsx +1 -1
  118. package/src/report/components/site-components/TraceWaterfall.tsx +1 -1
  119. package/src/report/components/site-components/compute.ts +6 -6
  120. package/src/report/components/site-components/index.tsx +7 -7
  121. package/src/report/components/site-components/scope-warnings.ts +3 -3
  122. package/src/report/components/site-components/snapshot-diagnostics.ts +1 -1
  123. package/src/report/components/summaries/ScopeSummary.tsx +2 -2
  124. package/src/report/components/summaries/compute.ts +2 -2
  125. package/src/report/components/summaries/faces.ts +1 -1
  126. package/src/report/components/summaries/index.tsx +1 -1
  127. package/src/report/index.ts +1 -1
  128. package/src/report/model/aggregate.ts +2 -2
  129. package/src/report/model/format.ts +2 -2
  130. package/src/report/model/metrics.ts +3 -3
  131. package/src/report/model/types.ts +12 -13
  132. package/src/results/annotated-source.test.ts +1 -1
  133. package/src/results/attempt-evidence.test.ts +1 -1
  134. package/src/results/copy.ts +3 -3
  135. package/src/results/format.ts +3 -3
  136. package/src/results/host-equivalence.test.ts +6 -6
  137. package/src/results/index.ts +1 -1
  138. package/src/results/locator.test.ts +2 -2
  139. package/src/results/locator.ts +1 -1
  140. package/src/results/open.ts +2 -2
  141. package/src/results/publish.ts +1 -1
  142. package/src/results/results.test.ts +6 -323
  143. package/src/results/select.test.ts +438 -0
  144. package/src/results/select.ts +6 -6
  145. package/src/results/skipped-notice.ts +1 -1
  146. package/src/results/truncate.ts +2 -2
  147. package/src/results/types.ts +9 -9
  148. package/src/results/writer.ts +4 -4
  149. package/src/runner/attempt.test.ts +2 -2
  150. package/src/runner/attempt.ts +6 -6
  151. package/src/runner/eval-source.test.ts +1 -1
  152. package/src/runner/eval-source.ts +1 -1
  153. package/src/runner/reporters/artifacts.ts +1 -1
  154. package/src/runner/run.ts +2 -2
  155. package/src/runner/timing.ts +1 -1
  156. package/src/runner/types.ts +14 -14
  157. package/src/sandbox/docker-agent-image.ts +36 -0
  158. package/src/sandbox/e2b-agent-template.test.ts +6 -21
  159. package/src/sandbox/e2b-agent-template.ts +44 -11
  160. package/src/sandbox/index.ts +8 -0
  161. package/src/sandbox/official-baselines.test.ts +175 -0
  162. package/src/sandbox/resolve.ts +1 -1
  163. package/src/sandbox/types.ts +1 -1
  164. package/src/scoring/diff.ts +1 -1
  165. package/src/scoring/types.ts +1 -1
  166. package/src/shared/facts.ts +1 -1
  167. package/src/show/index.ts +1 -1
  168. package/src/show/render.ts +1 -1
  169. package/src/show/show.test.ts +3 -3
  170. package/src/view/app/App.tsx +3 -1
  171. package/src/view/client-dist/app.css +1 -1
  172. package/src/view/client-dist/app.js +1 -1
  173. package/src/view/data.test.ts +2 -2
  174. package/src/view/data.ts +1 -1
  175. package/src/view/site-base.test.ts +51 -0
  176. package/src/view/site.ts +16 -0
  177. package/src/view/styles.css +1 -2
  178. package/src/view/template.html +1 -0
  179. 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
- DEFAULT_BUB_OVERRIDE,
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
- // TODO(upstream): BUB_OVERRIDE 钉在个人 fork 的修复分支上(tool-call 分支丢助手文本的修复
87
- // 尚未进上游,见 memory/bub-tapestore-otel…drift.md 台账),等上游包含后改回发布版。
88
- // 要试别的 pin 就改这两个常量——不设环境变量后门:装哪个包是配置,配置的家是代码
89
- // (边界见 docs/architecture.md「配置从代码来,凭据从环境来」)。
90
- const BUB_OVERRIDE = DEFAULT_BUB_OVERRIDE;
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
- // otel 插件跟上游 main 走(bub-contrib#50 起从 bub.tape 导入,要求 bub ≥ 0.3.10dev,
93
- // 与上面的 override 分支兼容)。插件不发 PyPI,git 依赖是唯一安装方式。
94
- const OTEL_PLUGIN = DEFAULT_BUB_OTEL_PLUGIN;
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, BUB_OVERRIDE, OTEL_PLUGIN);
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' '${BUB_OVERRIDE}' > ${BUB_OVERRIDE_FILE}`);
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 '${OTEL_PLUGIN}'${withPlugins ? ` ${withPlugins}` : ""}`,
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(
@@ -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[] = [claudeCodeDefault, codexDefault, bubDefault];
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();
@@ -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/results.md
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
@@ -1,5 +1,5 @@
1
1
  // 安装 Manifest 的落点与读写(定稿见 docs/feature/adapters/architecture/coding-agent-extensions.md
2
- // 「安装 Manifest」、docs/feature/results/architecture.md「agent-setup.json」)。
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/results.md
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
@@ -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(OpenClaw 默认走 Anthropic)。省略时读 ANTHROPIC_API_KEY env。 */
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
- coverage: defaultCoverage,
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
- ANTHROPIC_API_KEY: getApiKey(),
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。「最新 jsonl」而非按 session id 精确定位
126
- // ( claude-code 的裁决:send 串行,最新的一定是刚跑完的这次;超时 fallback 若产生
127
- // 第二条 run,也落在同一个最新 transcript 里,单次读取不重复采集)。
128
- const raw = await shared.captureLatestJsonl(sb, "~/.openclaw/agents");
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