@citeark/agent 0.3.20

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 (347) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +128 -0
  3. package/data/dataset-source-registry.v1.json +300 -0
  4. package/dist/arkgraph/boot.js +6 -0
  5. package/dist/arkgraph/index.html +1 -0
  6. package/dist/arkgraph/viewer.css +1 -0
  7. package/dist/arkgraph/viewer.en.css +1 -0
  8. package/dist/arkgraph/viewer.en.js +49 -0
  9. package/dist/arkgraph/viewer.en.js.LEGAL.txt +56 -0
  10. package/dist/arkgraph/viewer.js +49 -0
  11. package/dist/arkgraph/viewer.js.LEGAL.txt +56 -0
  12. package/docker/claude-code/Dockerfile +97 -0
  13. package/docker/claude-code/codex-pro-relay.mjs +466 -0
  14. package/docker/claude-code/runtime-contract-check.mjs +79 -0
  15. package/docs/arkgraph-reading.md +79 -0
  16. package/docs/configuration.md +100 -0
  17. package/docs/integration.md +92 -0
  18. package/docs/maturity-plan.md +27 -0
  19. package/docs/npm-release.md +44 -0
  20. package/docs/paper-reading.md +40 -0
  21. package/docs/research-plan-granularity.md +27 -0
  22. package/docs/terminal.md +49 -0
  23. package/examples/toy-evaluation/compile-task.json +27 -0
  24. package/examples/toy-evaluation/paper.md +5 -0
  25. package/examples/toy-evaluation/repository/README.md +9 -0
  26. package/examples/toy-evaluation/repository/checkpoint.json +4 -0
  27. package/examples/toy-evaluation/repository/evaluate.py +17 -0
  28. package/examples/toy-evaluation/task.json +81 -0
  29. package/package.json +59 -0
  30. package/prompts/compile-research.md +58 -0
  31. package/prompts/execute-contract.md +72 -0
  32. package/prompts/execute-workspace-simple.md +51 -0
  33. package/prompts/execute-workspace.md +34 -0
  34. package/prompts/prepare-reproduction.md +82 -0
  35. package/prompts/repair-research.md +45 -0
  36. package/protocol/CAP.md +129 -0
  37. package/protocol/LICENSE +12 -0
  38. package/protocol/MAPPINGS.md +72 -0
  39. package/protocol/README.md +38 -0
  40. package/protocol/conformance-v2.0-alpha.1.json +36 -0
  41. package/protocol/examples/arkgraph/checkpoint-evaluation.json +309 -0
  42. package/protocol/examples/arkgraph/fixtures.mjs +49 -0
  43. package/protocol/examples/arkgraph/paper-free.json +291 -0
  44. package/protocol/examples/arkgraph/partial-failure.json +344 -0
  45. package/protocol/examples/arkgraph/training-evaluation.json +443 -0
  46. package/protocol/profiles/agent-trace.md +16 -0
  47. package/protocol/profiles/computational-run.md +16 -0
  48. package/protocol/profiles/core.md +15 -0
  49. package/protocol/profiles/public-bundle.md +18 -0
  50. package/protocol/profiles/reproduction.md +29 -0
  51. package/protocol/profiles/research-compilation.md +44 -0
  52. package/protocol/profiles/research-plan.md +39 -0
  53. package/protocol/profiles/restricted-evidence.md +15 -0
  54. package/runtime/bootstrap-autodl-runtime.sh +314 -0
  55. package/runtime/create-runtime-venv.sh +41 -0
  56. package/runtime/install-local-cpu-runtime.sh +23 -0
  57. package/runtime/install-scientific-runtime.sh +153 -0
  58. package/runtime/mineru/parse.py +62 -0
  59. package/runtime/mineru/requirements.txt +4 -0
  60. package/runtime/requirements-baseline.txt +38 -0
  61. package/schemas/cap/v2/activity.schema.json +47 -0
  62. package/schemas/cap/v2/agent.schema.json +32 -0
  63. package/schemas/cap/v2/assertion.schema.json +110 -0
  64. package/schemas/cap/v2/descriptor.schema.json +243 -0
  65. package/schemas/cap/v2/entity.schema.json +64 -0
  66. package/schemas/cap/v2/manifest.schema.json +67 -0
  67. package/schemas/cap/v2/relation.schema.json +82 -0
  68. package/schemas/compute-catalog.schema.json +63 -0
  69. package/schemas/compute-decision.schema.json +27 -0
  70. package/schemas/execution-contract.schema.json +1024 -0
  71. package/schemas/research-card.schema.json +30 -0
  72. package/schemas/research-inventory-draft.schema.json +366 -0
  73. package/schemas/research.schema.json +1044 -0
  74. package/schemas/result.schema.json +173 -0
  75. package/schemas/verification-policy.schema.json +47 -0
  76. package/schemas/verified-conclusion.schema.json +58 -0
  77. package/schemas/workspace-summary.schema.json +24 -0
  78. package/scripts/build-arkgraph-view.mjs +12 -0
  79. package/scripts/check-execution-feasibility.mjs +24 -0
  80. package/scripts/check-syntax.mjs +15 -0
  81. package/scripts/deterministic-asset-preparation.py +438 -0
  82. package/scripts/package-cap.mjs +23 -0
  83. package/scripts/package-local-agent.mjs +23 -0
  84. package/scripts/preview-arkgraph.mjs +25 -0
  85. package/scripts/replay-research-compiler-candidate.mjs +134 -0
  86. package/scripts/review-compiler-sources.mjs +44 -0
  87. package/scripts/run-asset-preparation.sh +17 -0
  88. package/scripts/run-research-plan.mjs +98 -0
  89. package/scripts/validate-asset-preparation.py +290 -0
  90. package/scripts/verify-local-runtime.mjs +57 -0
  91. package/scripts/verify-npm-package.mjs +57 -0
  92. package/src/adapters/paper2agent.mjs +107 -0
  93. package/src/assets/cache.mjs +159 -0
  94. package/src/assets/compute.mjs +98 -0
  95. package/src/assets/executor.mjs +145 -0
  96. package/src/assets/lifecycle.mjs +213 -0
  97. package/src/assets/manifest.mjs +242 -0
  98. package/src/assets/opportunistic-preparation.mjs +81 -0
  99. package/src/assets/plan.mjs +411 -0
  100. package/src/assets/prompts.mjs +29 -0
  101. package/src/assets/public-asset-probe.mjs +525 -0
  102. package/src/assets/qualification.mjs +119 -0
  103. package/src/assets/readiness.mjs +130 -0
  104. package/src/assets/reproduction-admission.mjs +355 -0
  105. package/src/assets/requirements.mjs +152 -0
  106. package/src/assets/source-grounding.mjs +341 -0
  107. package/src/assets/source-policy.mjs +118 -0
  108. package/src/autodl/client.mjs +260 -0
  109. package/src/autodl/ssh.mjs +380 -0
  110. package/src/autodl/tools.mjs +129 -0
  111. package/src/cap/redaction.mjs +38 -0
  112. package/src/cap/v2/archive.mjs +152 -0
  113. package/src/cap/v2/attestation.mjs +204 -0
  114. package/src/cap/v2/canonical-json.mjs +114 -0
  115. package/src/cap/v2/compilation-artifact.mjs +240 -0
  116. package/src/cap/v2/core.mjs +282 -0
  117. package/src/cap/v2/measurement-assessment-records.mjs +23 -0
  118. package/src/cap/v2/pipeline-artifact.mjs +922 -0
  119. package/src/cap/v2/read.mjs +41 -0
  120. package/src/cap/v2/reassessment-artifact.mjs +383 -0
  121. package/src/cap/v2/research-artifact.mjs +231 -0
  122. package/src/cap/v2/research-map-records.mjs +46 -0
  123. package/src/cap/v2/research-object-records.mjs +163 -0
  124. package/src/cap/v2/research-records.mjs +187 -0
  125. package/src/cap/v2/verify.mjs +642 -0
  126. package/src/cli.mjs +1146 -0
  127. package/src/compute/autodl-pro-compiler.mjs +347 -0
  128. package/src/compute/autodl-pro-executor.mjs +459 -0
  129. package/src/compute/autodl-pro-job.mjs +843 -0
  130. package/src/compute/autodl-pro-network.mjs +295 -0
  131. package/src/compute/autodl-pro-remote.mjs +810 -0
  132. package/src/compute/autodl-pro-staging.mjs +117 -0
  133. package/src/compute/campaign.mjs +110 -0
  134. package/src/compute/catalog.mjs +123 -0
  135. package/src/compute/checkpoint-protocol.mjs +154 -0
  136. package/src/compute/codex-account-lock.mjs +111 -0
  137. package/src/compute/codex-account-session.mjs +107 -0
  138. package/src/compute/compiler-profile.mjs +38 -0
  139. package/src/compute/compiler-router.mjs +23 -0
  140. package/src/compute/coordinator-recovery.mjs +210 -0
  141. package/src/compute/executor-router.mjs +29 -0
  142. package/src/compute/gcp-batch-compiler.mjs +685 -0
  143. package/src/compute/gcp-batch-executor.mjs +1215 -0
  144. package/src/compute/gcp-batch-failure.mjs +92 -0
  145. package/src/compute/gcp-batch-job.mjs +527 -0
  146. package/src/compute/gcp-batch-lifecycle.mjs +81 -0
  147. package/src/compute/gcp-checkpoint-worker.mjs +1633 -0
  148. package/src/compute/local-codex-compiler.mjs +52 -0
  149. package/src/compute/measurement-hardware.mjs +128 -0
  150. package/src/compute/remote-attempt.mjs +226 -0
  151. package/src/compute/requirements.mjs +124 -0
  152. package/src/compute/research-phases.mjs +48 -0
  153. package/src/compute/scheduler.mjs +452 -0
  154. package/src/compute/shared-workloads.mjs +26 -0
  155. package/src/compute/stage-archive.mjs +79 -0
  156. package/src/contracts/campaign-contract.mjs +52 -0
  157. package/src/contracts/execution-contract.mjs +819 -0
  158. package/src/contracts/execution-mode.mjs +19 -0
  159. package/src/contracts/execution-timeouts.mjs +45 -0
  160. package/src/contracts/execution-workload.mjs +68 -0
  161. package/src/contracts/preflight-schema.mjs +25 -0
  162. package/src/contracts/public-contract.mjs +63 -0
  163. package/src/contracts/subject-tags.mjs +31 -0
  164. package/src/dashboard/data.mjs +898 -0
  165. package/src/dashboard/server.mjs +79 -0
  166. package/src/dashboard/static/dashboard.css +366 -0
  167. package/src/dashboard/static/dashboard.js +560 -0
  168. package/src/dashboard/static/index.html +85 -0
  169. package/src/deployment/community-policy.mjs +9 -0
  170. package/src/deployment/environment.mjs +112 -0
  171. package/src/deployment/guided.mjs +98 -0
  172. package/src/deployment/handoff.mjs +102 -0
  173. package/src/deployment/local-contract.mjs +31 -0
  174. package/src/deployment/local.mjs +100 -0
  175. package/src/deployment/prepare.mjs +46 -0
  176. package/src/deployment/recipe.mjs +108 -0
  177. package/src/deployment/supplement.mjs +51 -0
  178. package/src/deployment/terminal.mjs +43 -0
  179. package/src/diagnosis/renderer.mjs +75 -0
  180. package/src/diagnosis/target-failure.mjs +46 -0
  181. package/src/evidence/parser-registry.mjs +54 -0
  182. package/src/evidence/parsers/fasttext-classification.mjs +82 -0
  183. package/src/evidence/parsers/json-scalar.mjs +96 -0
  184. package/src/evidence/parsers/simcse-senteval.mjs +104 -0
  185. package/src/evidence/parsers/starspace-classification.mjs +78 -0
  186. package/src/evidence/registry.mjs +147 -0
  187. package/src/execution/runner-audit.mjs +473 -0
  188. package/src/gcp/auth.mjs +106 -0
  189. package/src/gcp/batch-client.mjs +120 -0
  190. package/src/gcp/resource-discovery.mjs +177 -0
  191. package/src/gcp/rest.mjs +82 -0
  192. package/src/gcp/secret-manager.mjs +34 -0
  193. package/src/gcp/signed-url.mjs +133 -0
  194. package/src/gcp/storage.mjs +220 -0
  195. package/src/graph/command.mjs +41 -0
  196. package/src/graph/execution.mjs +97 -0
  197. package/src/graph/model.mjs +37 -0
  198. package/src/graph/presentation.mjs +110 -0
  199. package/src/graph/query.mjs +159 -0
  200. package/src/graph/research-relations.mjs +69 -0
  201. package/src/graph/source-page.mjs +12 -0
  202. package/src/graph/source-preview.mjs +34 -0
  203. package/src/graph/validate.mjs +76 -0
  204. package/src/job.mjs +496 -0
  205. package/src/network/autodl-routing-proxy.mjs +462 -0
  206. package/src/network/egress-proxy.mjs +158 -0
  207. package/src/observability/event-contract.mjs +230 -0
  208. package/src/observability/pipeline-monitor.mjs +166 -0
  209. package/src/pipeline/orchestrator.mjs +1281 -0
  210. package/src/pipeline/recovery-error.mjs +11 -0
  211. package/src/pipeline/replay.mjs +304 -0
  212. package/src/pipeline/shared-execution.mjs +115 -0
  213. package/src/pipeline/stage-checkpoint.mjs +86 -0
  214. package/src/pipeline/stage-recovery.mjs +101 -0
  215. package/src/pipeline/targets.mjs +110 -0
  216. package/src/process.mjs +143 -0
  217. package/src/protocol.mjs +312 -0
  218. package/src/provider/codex-account.mjs +44 -0
  219. package/src/provider/codex-completion.mjs +49 -0
  220. package/src/provider/completion.mjs +292 -0
  221. package/src/provider/model-client.mjs +44 -0
  222. package/src/provider/model-route.mjs +29 -0
  223. package/src/provider/openrouter-readiness.mjs +189 -0
  224. package/src/provider/reader-bridge.mjs +35 -0
  225. package/src/provider/relay.mjs +263 -0
  226. package/src/provider/runtime-auth.mjs +40 -0
  227. package/src/public/cap.d.mts +90 -0
  228. package/src/public/cap.mjs +12 -0
  229. package/src/public/contracts.d.mts +2 -0
  230. package/src/public/host.mjs +171 -0
  231. package/src/public/operations.d.mts +11 -0
  232. package/src/public/presentation.d.mts +4 -0
  233. package/src/records/views.mjs +26 -0
  234. package/src/remote/command.mjs +178 -0
  235. package/src/remote/ssh.mjs +59 -0
  236. package/src/repository-origin.mjs +81 -0
  237. package/src/reproduction/evidence-feedback.mjs +96 -0
  238. package/src/reproduction/incomplete-initialization.mjs +25 -0
  239. package/src/reproduction/lifecycle.mjs +253 -0
  240. package/src/reproduction/plan.mjs +132 -0
  241. package/src/reproduction/prompts.mjs +70 -0
  242. package/src/reproduction/runner.mjs +188 -0
  243. package/src/reproduction/summary.mjs +130 -0
  244. package/src/reproduction/workspace-mode.mjs +7 -0
  245. package/src/research/automatic-admission.mjs +156 -0
  246. package/src/research/compiler-coverage.mjs +85 -0
  247. package/src/research/compiler-failure.mjs +24 -0
  248. package/src/research/compiler-normalization-guards.mjs +112 -0
  249. package/src/research/compiler-repair.mjs +3 -0
  250. package/src/research/compiler.mjs +853 -0
  251. package/src/research/continuation-selection.mjs +26 -0
  252. package/src/research/execution-graph-context.mjs +43 -0
  253. package/src/research/experiment-importance.mjs +15 -0
  254. package/src/research/inventory-handoff.mjs +104 -0
  255. package/src/research/inventory-revisions.mjs +32 -0
  256. package/src/research/mineru-local.mjs +73 -0
  257. package/src/research/paper-command.mjs +19 -0
  258. package/src/research/paper-markdown.mjs +180 -0
  259. package/src/research/paper-source-map.mjs +69 -0
  260. package/src/research/planning-policy.mjs +88 -0
  261. package/src/research/reference-materials.mjs +11 -0
  262. package/src/research/reproduction-scope.mjs +30 -0
  263. package/src/research/research-map.mjs +94 -0
  264. package/src/research/research-objects.mjs +88 -0
  265. package/src/research/source-discovery.mjs +646 -0
  266. package/src/research/source-observations.mjs +75 -0
  267. package/src/research/source-review-cli-mcp.mjs +26 -0
  268. package/src/research/source-review-input.mjs +209 -0
  269. package/src/research/source-review-local-codex.mjs +36 -0
  270. package/src/research/source-review-model.mjs +70 -0
  271. package/src/research/source-review.mjs +173 -0
  272. package/src/research/structure.mjs +3163 -0
  273. package/src/research-card/renderer.mjs +277 -0
  274. package/src/research-card/verified-conclusion.mjs +143 -0
  275. package/src/results/output-registry.mjs +183 -0
  276. package/src/runtime/claude-code.mjs +52 -0
  277. package/src/runtime/codex-capacity-retry.mjs +87 -0
  278. package/src/runtime/codex.mjs +64 -0
  279. package/src/runtime/config.mjs +157 -0
  280. package/src/runtime/final-output.mjs +40 -0
  281. package/src/runtime/index.mjs +21 -0
  282. package/src/runtime/local-codex.mjs +74 -0
  283. package/src/runtime/opencode.mjs +95 -0
  284. package/src/runtime/prompt.mjs +13 -0
  285. package/src/sandbox/docker.mjs +363 -0
  286. package/src/settings/command.mjs +297 -0
  287. package/src/settings/store.mjs +119 -0
  288. package/src/telemetry/pricing.mjs +68 -0
  289. package/src/telemetry/usage.mjs +265 -0
  290. package/src/terminal/events.mjs +97 -0
  291. package/src/terminal/input.mjs +40 -0
  292. package/src/terminal/plain.mjs +40 -0
  293. package/src/terminal/remote-stream.mjs +22 -0
  294. package/src/terminal/screen.mjs +214 -0
  295. package/src/terminal/transcript.mjs +69 -0
  296. package/src/util.mjs +107 -0
  297. package/src/verification/ai-assessor.mjs +534 -0
  298. package/src/verification/claim-evaluator.mjs +242 -0
  299. package/src/verification/evidence-context.mjs +165 -0
  300. package/src/verification/evidence-reader.mjs +95 -0
  301. package/src/verification/integrity.mjs +570 -0
  302. package/src/verification/tolerance.mjs +32 -0
  303. package/src/workloads/cpu-research-preparation.mjs +56 -0
  304. package/src/workloads/definition.mjs +74 -0
  305. package/src/workloads/phase-aware-reproduction.mjs +46 -0
  306. package/src/workloads/reproduction.mjs +85 -0
  307. package/src/workspace/command.mjs +242 -0
  308. package/src/workspace/control.mjs +49 -0
  309. package/src/workspace/entry.mjs +28 -0
  310. package/src/workspace/input.mjs +93 -0
  311. package/src/workspace/interactive.mjs +94 -0
  312. package/src/workspace/jobs.mjs +418 -0
  313. package/src/workspace/session.mjs +97 -0
  314. package/src/workspace/worker.mjs +137 -0
  315. package/ui/arkgraph/ambient-motion.mjs +10 -0
  316. package/ui/arkgraph/app.jsx +153 -0
  317. package/ui/arkgraph/boot.js +6 -0
  318. package/ui/arkgraph/camera-motion.mjs +20 -0
  319. package/ui/arkgraph/context-reveal.mjs +39 -0
  320. package/ui/arkgraph/details.css +3 -0
  321. package/ui/arkgraph/entry.jsx +28 -0
  322. package/ui/arkgraph/experiment-curves.mjs +17 -0
  323. package/ui/arkgraph/experiment-selection.mjs +15 -0
  324. package/ui/arkgraph/experiment-style.css +26 -0
  325. package/ui/arkgraph/experiment-ui.jsx +32 -0
  326. package/ui/arkgraph/frame.html +1 -0
  327. package/ui/arkgraph/graph-gestures.mjs +62 -0
  328. package/ui/arkgraph/label-layout.mjs +57 -0
  329. package/ui/arkgraph/locales/en.json +229 -0
  330. package/ui/arkgraph/locales/source-types.json +15 -0
  331. package/ui/arkgraph/localization-build.mjs +27 -0
  332. package/ui/arkgraph/material-build.mjs +23 -0
  333. package/ui/arkgraph/material-colors.mjs +39 -0
  334. package/ui/arkgraph/material-style.css +15 -0
  335. package/ui/arkgraph/open-graph.jsx +326 -0
  336. package/ui/arkgraph/outline.jsx +49 -0
  337. package/ui/arkgraph/package-lock.json +888 -0
  338. package/ui/arkgraph/package.json +17 -0
  339. package/ui/arkgraph/reading-layout.mjs +130 -0
  340. package/ui/arkgraph/reading-presentation.mjs +73 -0
  341. package/ui/arkgraph/record-detail.css +51 -0
  342. package/ui/arkgraph/record-details.jsx +29 -0
  343. package/ui/arkgraph/research-types.mjs +31 -0
  344. package/ui/arkgraph/selection-mark.jsx +6 -0
  345. package/ui/arkgraph/soft-spine.mjs +26 -0
  346. package/ui/arkgraph/steering-style.css +187 -0
  347. package/ui/arkgraph/style.css +272 -0
@@ -0,0 +1,898 @@
1
+ import { open, readFile, readdir, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { metricMeasurements, primaryComparison, primaryMetric } from "../records/views.mjs";
5
+ import { estimateDeepSeekCost } from "../telemetry/pricing.mjs";
6
+ import { mergeAgentUsage, readAgentUsage } from "../telemetry/usage.mjs";
7
+
8
+ const STAGE_DEFINITIONS = [
9
+ ["research_compilation", "研究结构化", "从论文与仓库提取可执行的研究对象"],
10
+ ["research_plan", "研究计划快照", "固定并验证不可变 Research Plan CAP"],
11
+ ["contract_creation", "执行契约", "固定 Claim、实验、环境与验证边界"],
12
+ ["compute_scheduling", "计算调度", "根据实验需求、预算和资源目录选择执行规格"],
13
+ ["reproduction", "自主复现", "Agent 在隔离环境中执行与修复实验"],
14
+ ["evidence_parsing", "证据解析", "从原始输出中确定性解析指标"],
15
+ ["verification", "结果验证", "检查完整性并比较论文报告结果"],
16
+ ["research_card", "验证结论与卡片", "汇总论文级验证结论并生成展示快照"],
17
+ ["packaging", "产物封装", "生成 CAP 2.0 Records、Blobs 与签名证明"],
18
+ ["archive", "单文件封装", "生成可上传和可独立校验的 .cap 文件"],
19
+ ];
20
+
21
+ const HEARTBEAT_HEALTHY_MS = 45_000;
22
+ const HEARTBEAT_STALE_MS = 120_000;
23
+ const OUTPUT_QUIET_MS = 5 * 60_000;
24
+ const OUTPUT_ATTENTION_MS = 15 * 60_000;
25
+
26
+ export async function listPipelineDashboard(pipelinesDirectory, { generatedAt = new Date().toISOString() } = {}) {
27
+ const root = path.resolve(pipelinesDirectory);
28
+ let entries = [];
29
+ try {
30
+ entries = await readdir(root, { withFileTypes: true });
31
+ } catch {
32
+ return { generatedAt, pipelinesDirectory: root, pipelines: [], totals: emptyTotals() };
33
+ }
34
+
35
+ const pipelines = (
36
+ await Promise.all(
37
+ entries
38
+ .filter((entry) => entry.isDirectory())
39
+ .map((entry) => readPipelineDashboard(root, entry.name, { generatedAt })),
40
+ )
41
+ )
42
+ .filter(Boolean)
43
+ .sort((left, right) => Date.parse(right.activity?.lastActivityAt ?? right.updatedAt) - Date.parse(left.activity?.lastActivityAt ?? left.updatedAt));
44
+
45
+ return {
46
+ generatedAt,
47
+ pipelinesDirectory: root,
48
+ pipelines,
49
+ totals: summarizePipelines(pipelines),
50
+ };
51
+ }
52
+
53
+ export async function readPipelineDashboard(pipelinesDirectory, directoryName, { generatedAt = new Date().toISOString() } = {}) {
54
+ const root = path.resolve(pipelinesDirectory, directoryName);
55
+ const pipeline = await safeJson(path.join(root, "pipeline.json"));
56
+ if (!pipeline?.pipelineId) return null;
57
+
58
+ const [compilationRecord, reproductionRecord, research, metrics, assessment, integrity, manifest, compileTask, runtimeTask, computeDecision, researchCard, verifiedConclusion] =
59
+ await Promise.all([
60
+ readNestedRunRecord(root, "compilations"),
61
+ readNestedRunRecord(root, "runs"),
62
+ firstJson([pipeline.researchPath]),
63
+ safeJson(path.join(root, "cap", "projections", "citeark", "metrics.json")),
64
+ safeJson(path.join(root, "cap", "projections", "citeark", "assessment.json")),
65
+ safeJson(path.join(root, "cap", "projections", "citeark", "integrity.json")),
66
+ safeJson(path.join(root, "cap", "cap-manifest.json")),
67
+ readNestedJson(root, "compilations", "input/submitted-compile-task.json"),
68
+ readNestedJson(root, "runs", "input/submitted-task.json"),
69
+ firstJson([
70
+ path.join(root, "cap", "protocol", "compute-decision.json"),
71
+ path.join(root, "protocol", "compute-decision.json"),
72
+ ]),
73
+ safeJson(path.join(root, "cap", "projections", "citeark", "research-card.json")),
74
+ safeJson(path.join(root, "cap", "projections", "citeark", "verified-conclusion.json")),
75
+ ]);
76
+ const compilationRun = compilationRecord?.run ?? null;
77
+ const reproductionRun = reproductionRecord?.run ?? null;
78
+
79
+ const [discoveredCompilationUsage, discoveredReproductionUsage] = await Promise.all([
80
+ usageForNestedRun(root, "compilations"),
81
+ usageForNestedRun(root, "runs"),
82
+ ]);
83
+ const measuredUsage = mergeAgentUsage(
84
+ preferMeasuredUsage(compilationRun?.usage, discoveredCompilationUsage),
85
+ preferMeasuredUsage(reproductionRun?.usage, discoveredReproductionUsage),
86
+ );
87
+ const usage = preferMeasuredUsage(pipeline.usage, measuredUsage);
88
+ const task = compileTask ?? runtimeTask ?? {};
89
+ const selectedAgent = pipeline.agent ?? task.agent ?? {};
90
+ const pricedUsage = {
91
+ ...usage,
92
+ pricing: pricingFor(usage, selectedAgent.model),
93
+ availability: usageAvailability(usage),
94
+ };
95
+ const stages = normalizeStages(pipeline, compilationRun, reproductionRun, generatedAt);
96
+ const activity = await readPipelineActivity({
97
+ root,
98
+ pipeline,
99
+ stages,
100
+ compilationRecord,
101
+ reproductionRecord,
102
+ generatedAt,
103
+ });
104
+ const start = pipeline.createdAt;
105
+ const end = pipeline.status === "running"
106
+ ? generatedAt
107
+ : pipeline.finishedAt ?? pipeline.updatedAt ?? generatedAt;
108
+ const completedStages = stages.filter((stage) => stage.status === "completed" || stage.status === "skipped").length;
109
+ const assessmentView = assessment?.citeark
110
+ ? { ...assessment.citeark, verdict: assessment.conclusion }
111
+ : assessment;
112
+ const comparison = primaryComparison(assessmentView);
113
+ const firstMeasurement = metricMeasurements(metrics)[0] ?? null;
114
+ const capFiles = [
115
+ ...(manifest?.records ?? []),
116
+ ...(manifest?.blobs ?? []),
117
+ ];
118
+
119
+ return {
120
+ id: pipeline.pipelineId,
121
+ directoryName,
122
+ title: research?.work?.title ?? compileTask?.paper?.title ?? task.name ?? pipeline.pipelineId,
123
+ workId: research?.work?.id ?? null,
124
+ claim: research?.claims?.find((claim) => claim.id === pipeline.claimId) ?? research?.claims?.[0] ?? null,
125
+ experiment: research?.experiments?.find((experiment) => experiment.id === pipeline.experimentId) ?? research?.experiments?.[0] ?? null,
126
+ status: pipeline.status,
127
+ phase: pipeline.phase,
128
+ verificationStatus: pipeline.verificationStatus ?? assessmentView?.verificationStatus ?? null,
129
+ integrityStatus: integrity?.status ?? null,
130
+ publicationStatus: manifest ? "publishable" : null,
131
+ createdAt: pipeline.createdAt,
132
+ updatedAt: latestTimestamp(pipeline.updatedAt, activity.lastActivityAt),
133
+ recordUpdatedAt: pipeline.updatedAt,
134
+ finishedAt: pipeline.finishedAt ?? null,
135
+ durationMs: validDuration(start, end),
136
+ progressPercent: Math.round((completedStages / stages.length) * 100),
137
+ stages,
138
+ activity,
139
+ usage: pricedUsage,
140
+ comparison,
141
+ metric: primaryMetric(metrics),
142
+ parser: firstMeasurement?.parser ?? null,
143
+ sourceEvidence: firstMeasurement?.sourceEvidence ?? null,
144
+ agent: {
145
+ runtime: selectedAgent.runtime ?? reproductionRun?.runtime ?? compilationRun?.runtime ?? null,
146
+ provider: selectedAgent.api?.protocol ?? null,
147
+ model: selectedAgent.model ?? firstModel(pricedUsage) ?? null,
148
+ subagentModel: selectedAgent.subagentModel ?? null,
149
+ effort: selectedAgent.effort ?? null,
150
+ turns: pricedUsage.turns ?? 0,
151
+ },
152
+ environment: {
153
+ sandbox: reproductionRun?.sandbox ?? compilationRun?.sandbox ?? null,
154
+ image: reproductionRun?.executionEnvironment?.image ?? computeDecision?.environment?.image ?? task.environment?.image ?? reproductionRun?.image ?? compilationRun?.image ?? null,
155
+ cpus: reproductionRun?.executionEnvironment?.cpus ?? computeDecision?.environment?.cpus ?? task.environment?.cpus ?? null,
156
+ memoryGb: reproductionRun?.executionEnvironment?.memoryGb ?? computeDecision?.environment?.memoryGb ?? task.environment?.memoryGb ?? null,
157
+ gpu: reproductionRun?.executionEnvironment?.gpu ?? computeDecision?.environment?.gpu ?? task.environment?.gpu ?? null,
158
+ networkAccess: reproductionRun?.executionEnvironment?.networkAccess ?? computeDecision?.environment?.networkAccess ?? task.environment?.networkAccess ?? null,
159
+ cloud: publicCloudView(reproductionRun?.cloudExecution),
160
+ },
161
+ compute: computeDecision ? {
162
+ decisionDigest: computeDecision.decisionDigest,
163
+ classification: computeDecision.requirement?.classification ?? null,
164
+ requirement: computeDecision.requirement ?? null,
165
+ selectedProfile: computeDecision.selectedProfile ?? null,
166
+ estimate: computeDecision.estimate ?? null,
167
+ rationale: computeDecision.rationale ?? null,
168
+ alternatives: computeDecision.alternatives ?? [],
169
+ } : pipeline.computeDecision ?? null,
170
+ researchCard: researchCard ? {
171
+ cardDigest: researchCard.cardDigest,
172
+ generatedAt: researchCard.generatedAt,
173
+ available: true,
174
+ readmePath: "preview/README.md",
175
+ jsonPath: "projections/citeark/research-card.json",
176
+ } : pipeline.researchCard ?? { available: false },
177
+ verifiedConclusion: verifiedConclusion ?? researchCard?.verifiedConclusion ?? null,
178
+ attempts: [
179
+ ...executionAttemptRows("研究结构化", compilationRun),
180
+ ...executionAttemptRows("自主复现", reproductionRun),
181
+ ],
182
+ resilience: cloudResilience(reproductionRun?.cloudExecution),
183
+ artifacts: {
184
+ artifactDigest: pipeline.artifactDigest ?? null,
185
+ fileCount: capFiles.length,
186
+ totalBytes: capFiles.reduce((total, file) => total + Number(file.size ?? 0), 0),
187
+ files: capFiles.map((file) => ({ path: file.path ?? null, size: file.size, digest: file.digest })),
188
+ attestation: Boolean(manifest),
189
+ researchCard: Boolean(researchCard),
190
+ verifiedConclusion: Boolean(verifiedConclusion),
191
+ readme: Boolean(researchCard),
192
+ archive: pipeline.citearkArchive ?? null,
193
+ },
194
+ checks: integrity?.checks ?? [],
195
+ error: pipeline.error ?? null,
196
+ };
197
+ }
198
+
199
+ function normalizeStages(pipeline, compilationRun, reproductionRun, generatedAt = new Date().toISOString()) {
200
+ if (Array.isArray(pipeline.stages)) {
201
+ return STAGE_DEFINITIONS.map(([id, label, description]) => {
202
+ const stored = pipeline.stages.find((stage) => stage.id === id) ?? {
203
+ status: "pending",
204
+ startedAt: null,
205
+ finishedAt: null,
206
+ durationMs: null,
207
+ };
208
+ const normalized = { id, label, description, timingSource: "recorded", ...stored };
209
+ if (normalized.status === "running" && normalized.startedAt) {
210
+ normalized.durationMs = validDuration(normalized.startedAt, generatedAt);
211
+ }
212
+ return normalized;
213
+ });
214
+ }
215
+
216
+ const rank = STAGE_DEFINITIONS.findIndex(([id]) => id === pipeline.phase);
217
+ return STAGE_DEFINITIONS.map(([id, label, description], index) => {
218
+ const exactRun = id === "research_compilation" ? compilationRun : id === "reproduction" ? reproductionRun : null;
219
+ let status = "pending";
220
+ if (exactRun) {
221
+ status = exactRun.status === "completed"
222
+ ? "completed"
223
+ : exactRun.status === "running"
224
+ ? "running"
225
+ : exactRun.status === "cancelled"
226
+ ? "cancelled"
227
+ : "failed";
228
+ }
229
+ else if (pipeline.status === "completed") status = "completed";
230
+ else if (pipeline.status === "failed" && index === rank) status = "failed";
231
+ else if (pipeline.status === "cancelled" && index === rank) status = "cancelled";
232
+ else if (index < rank) status = "completed";
233
+ else if (index === rank && pipeline.status === "running") status = "running";
234
+
235
+ const startedAt = exactRun?.startedAt ?? null;
236
+ const finishedAt = exactRun?.finishedAt ?? null;
237
+ return {
238
+ id,
239
+ label,
240
+ description,
241
+ status,
242
+ startedAt,
243
+ finishedAt,
244
+ durationMs: exactRun ? validDuration(startedAt, finishedAt ?? (status === "running" ? generatedAt : null)) : null,
245
+ timingSource: exactRun ? "recorded" : "derived",
246
+ };
247
+ });
248
+ }
249
+
250
+ async function readNestedRunRecord(root, group) {
251
+ let entries = [];
252
+ try {
253
+ entries = await readdir(path.join(root, group), { withFileTypes: true });
254
+ } catch {
255
+ return null;
256
+ }
257
+ for (const entry of entries.sort((left, right) => right.name.localeCompare(left.name))) {
258
+ if (!entry.isDirectory()) continue;
259
+ const directory = path.join(root, group, entry.name);
260
+ const run = await safeJson(path.join(directory, "run.json"));
261
+ if (run) return { run, directory };
262
+ }
263
+ return null;
264
+ }
265
+
266
+ async function readNestedJson(root, group, relativePath) {
267
+ let entries = [];
268
+ try {
269
+ entries = await readdir(path.join(root, group), { withFileTypes: true });
270
+ } catch {
271
+ return null;
272
+ }
273
+ for (const entry of entries.sort((left, right) => right.name.localeCompare(left.name))) {
274
+ if (!entry.isDirectory()) continue;
275
+ const value = await safeJson(path.join(root, group, entry.name, relativePath));
276
+ if (value) return value;
277
+ }
278
+ return null;
279
+ }
280
+
281
+ async function usageForNestedRun(root, group) {
282
+ let entries = [];
283
+ try {
284
+ entries = await readdir(path.join(root, group), { withFileTypes: true });
285
+ } catch {
286
+ return null;
287
+ }
288
+ const values = [];
289
+ for (const entry of entries) {
290
+ if (!entry.isDirectory()) continue;
291
+ const runDirectory = path.join(root, group, entry.name);
292
+ const run = await safeJson(path.join(runDirectory, "run.json"));
293
+ values.push(await readAgentUsage(path.join(runDirectory, "execution", "trace.jsonl"), {
294
+ runtimeHome: path.join(runDirectory, "runtime-home"),
295
+ model: run?.agent?.model ?? null,
296
+ }));
297
+ }
298
+ return mergeAgentUsage(...values);
299
+ }
300
+
301
+ function preferMeasuredUsage(...values) {
302
+ return values
303
+ .filter(Boolean)
304
+ .sort((left, right) =>
305
+ Number(right.totalTokens ?? 0) - Number(left.totalTokens ?? 0)
306
+ || Number(right.turns ?? 0) - Number(left.turns ?? 0)
307
+ || Number(right.costUsd ?? 0) - Number(left.costUsd ?? 0)
308
+ )[0] ?? mergeAgentUsage();
309
+ }
310
+
311
+ async function readPipelineActivity({
312
+ root,
313
+ pipeline,
314
+ stages,
315
+ compilationRecord,
316
+ reproductionRecord,
317
+ generatedAt,
318
+ }) {
319
+ const [monitor, monitorEvents, compilationActivity, reproductionActivity] = await Promise.all([
320
+ safeJson(path.join(root, "observability", "progress.json")),
321
+ tailJsonRecords(path.join(root, "observability", "events.jsonl"), { maxBytes: 128 * 1024, maxLines: 40 }),
322
+ inspectRunActivity(compilationRecord?.directory),
323
+ inspectRunActivity(reproductionRecord?.directory),
324
+ ]);
325
+ const cloudActivity = inspectCloudActivity(reproductionRecord?.run);
326
+ const lastOutputAt = latestTimestamp(
327
+ monitor?.lastOutputAt,
328
+ compilationActivity.lastOutputAt,
329
+ reproductionActivity.lastOutputAt,
330
+ cloudActivity.lastOutputAt,
331
+ );
332
+ const lastActivityAt = latestTimestamp(
333
+ pipeline.updatedAt,
334
+ compilationRecord?.run?.updatedAt,
335
+ reproductionRecord?.run?.updatedAt,
336
+ monitor?.lastActivityAt,
337
+ lastOutputAt,
338
+ );
339
+ const lastHeartbeatAt = monitor?.lastHeartbeatAt ?? null;
340
+ const events = [
341
+ ...monitorEvents.records.map(publicMonitorEvent).filter(Boolean),
342
+ ...compilationActivity.events,
343
+ ...reproductionActivity.events,
344
+ ...cloudActivity.events,
345
+ ]
346
+ .filter(Boolean)
347
+ .sort((left, right) => eventSortTime(right) - eventSortTime(left) || Number(right.order ?? 0) - Number(left.order ?? 0))
348
+ .filter(uniqueEvent())
349
+ .slice(0, 16);
350
+ const health = classifyActivityHealth({
351
+ status: pipeline.status,
352
+ generatedAt,
353
+ lastHeartbeatAt,
354
+ lastActivityAt,
355
+ lastOutputAt,
356
+ hasMonitor: Boolean(monitor),
357
+ });
358
+ const activeStage = stages.find((stage) => stage.status === "running" || stage.status === "prepared")
359
+ ?? stages.find((stage) => stage.id === pipeline.phase)
360
+ ?? null;
361
+ return {
362
+ health: health.code,
363
+ healthLabel: health.label,
364
+ attentionRequired: health.attentionRequired,
365
+ controllerAlive: health.controllerAlive,
366
+ reason: health.reason,
367
+ currentAction: events.find((event) =>
368
+ event.source === "agent-trace"
369
+ && event.kind !== "checkpoint"
370
+ && event.kind !== "infrastructure"
371
+ )?.message ?? events[0]?.message ?? monitor?.currentAction ?? activeStage?.description ?? "等待新的执行记录",
372
+ currentStage: monitor?.currentStage ?? pipeline.phase ?? activeStage?.id ?? null,
373
+ currentStageStartedAt: activeStage?.startedAt ?? null,
374
+ lastHeartbeatAt,
375
+ lastActivityAt,
376
+ lastOutputAt,
377
+ heartbeatAgeMs: ageMs(generatedAt, lastHeartbeatAt),
378
+ activityAgeMs: ageMs(generatedAt, lastActivityAt),
379
+ outputAgeMs: ageMs(generatedAt, lastOutputAt),
380
+ source: monitor
381
+ ? "pipeline-heartbeat+execution-files+cloud-checkpoints"
382
+ : "execution-files+cloud-checkpoints",
383
+ refreshIntervalSeconds: 4,
384
+ events,
385
+ };
386
+ }
387
+
388
+ function inspectCloudActivity(run) {
389
+ const jobs = run?.cloudExecution?.jobs ?? [];
390
+ const activity = jobs.toReversed().find((job) => Array.isArray(job?.activity?.traceTail))?.activity ?? null;
391
+ if (!activity) return { lastOutputAt: null, events: [] };
392
+ const lastOutputAt = validTimestamp(activity.lastOutputAt);
393
+ const records = activity.traceTail.slice(-24).map((value, order) => ({ value, order }));
394
+ return {
395
+ lastOutputAt,
396
+ events: summarizeTraceEvents(records, lastOutputAt),
397
+ };
398
+ }
399
+
400
+ async function inspectRunActivity(directory) {
401
+ if (!directory) return { lastOutputAt: null, events: [] };
402
+ const tracePath = path.join(directory, "execution", "trace.jsonl");
403
+ const [trace, fixedFiles, logFiles, evidenceFiles] = await Promise.all([
404
+ tailJsonRecords(tracePath, { maxBytes: 256 * 1024, maxLines: 100 }),
405
+ newestFileTimestamp([
406
+ tracePath,
407
+ path.join(directory, "execution", "claude-stderr.log"),
408
+ path.join(directory, "output", "result.json"),
409
+ ]),
410
+ newestDirectoryFileTimestamp(path.join(directory, "output", "logs")),
411
+ newestDirectoryFileTimestamp(path.join(directory, "output", "evidence")),
412
+ ]);
413
+ return {
414
+ lastOutputAt: latestTimestamp(fixedFiles, logFiles, evidenceFiles, trace.modifiedAt),
415
+ events: summarizeTraceEvents(trace.records, trace.modifiedAt),
416
+ };
417
+ }
418
+
419
+ async function tailJsonRecords(target, { maxBytes, maxLines }) {
420
+ let handle;
421
+ try {
422
+ handle = await open(target, "r");
423
+ const info = await handle.stat();
424
+ const length = Math.min(info.size, maxBytes);
425
+ const start = Math.max(0, info.size - length);
426
+ const buffer = Buffer.alloc(length);
427
+ await handle.read(buffer, 0, length, start);
428
+ let lines = buffer.toString("utf8").split(/\r?\n/).filter(Boolean);
429
+ if (start > 0) lines = lines.slice(1);
430
+ const records = lines.slice(-maxLines).flatMap((line, index) => {
431
+ try {
432
+ return [{ value: JSON.parse(line), order: index }];
433
+ } catch {
434
+ return [];
435
+ }
436
+ });
437
+ return { records, modifiedAt: info.mtime.toISOString() };
438
+ } catch {
439
+ return { records: [], modifiedAt: null };
440
+ } finally {
441
+ await handle?.close().catch(() => {});
442
+ }
443
+ }
444
+
445
+ async function newestFileTimestamp(targets) {
446
+ const values = await Promise.all(targets.map(async (target) => {
447
+ try {
448
+ return (await stat(target)).mtime.toISOString();
449
+ } catch {
450
+ return null;
451
+ }
452
+ }));
453
+ return latestTimestamp(...values);
454
+ }
455
+
456
+ async function newestDirectoryFileTimestamp(directory) {
457
+ let entries = [];
458
+ try {
459
+ entries = await readdir(directory, { withFileTypes: true });
460
+ } catch {
461
+ return null;
462
+ }
463
+ return await newestFileTimestamp(entries.filter((entry) => entry.isFile()).map((entry) => path.join(directory, entry.name)));
464
+ }
465
+
466
+ function publicMonitorEvent(record) {
467
+ const event = record?.value;
468
+ if (!event || typeof event.message !== "string") return null;
469
+ return {
470
+ at: validTimestamp(event.at),
471
+ observedAt: validTimestamp(event.at),
472
+ kind: event.kind ?? "activity",
473
+ level: event.level ?? "info",
474
+ stage: event.stage ?? null,
475
+ message: cleanPublicText(event.message, 220),
476
+ order: event.sequence ?? record.order,
477
+ source: "pipeline-monitor",
478
+ };
479
+ }
480
+
481
+ function summarizeTraceEvents(records, observedAt) {
482
+ const events = [];
483
+ for (const record of records) {
484
+ const value = record.value;
485
+ const at = validTimestamp(value?.timestamp ?? value?.recorded_at ?? value?.created_at);
486
+ const base = {
487
+ at,
488
+ observedAt: at ?? observedAt,
489
+ stage: null,
490
+ order: record.order,
491
+ source: "agent-trace",
492
+ };
493
+ if (value?.type === "citeark.attempt_started") {
494
+ events.push({ ...base, kind: "attempt", level: "info", message: `Agent 开始第 ${value.attempt ?? "—"} 次执行` });
495
+ continue;
496
+ }
497
+ if (value?.type === "citeark.attempt_finished") {
498
+ const failed = Number(value.exit_code) !== 0 || value.timed_out === true;
499
+ events.push({
500
+ ...base,
501
+ kind: "attempt",
502
+ level: failed ? "error" : "success",
503
+ message: value.timed_out ? "Agent 执行超时" : failed ? `Agent 执行结束,退出码 ${value.exit_code}` : "Agent 执行成功结束",
504
+ });
505
+ continue;
506
+ }
507
+ if (value?.type === "citeark.checkpoint_saved") {
508
+ events.push({
509
+ ...base,
510
+ kind: "checkpoint",
511
+ level: "success",
512
+ message: `已保存${value.kind === "full" ? "完整" : "关键"}检查点${value.bytes ? `(${value.bytes} 字节)` : ""}`,
513
+ });
514
+ continue;
515
+ }
516
+ if (value?.type === "citeark.checkpoint_restored") {
517
+ events.push({ ...base, kind: "checkpoint", level: "warning", message: `基础设施重启,已从检查点恢复第 ${Number(value.infrastructureRetry ?? 0) + 1} 次执行` });
518
+ continue;
519
+ }
520
+ if (value?.type === "citeark.infrastructure_interruption") {
521
+ events.push({ ...base, kind: "infrastructure", level: "warning", message: "云端执行收到中断信号,正在紧急持久化关键状态" });
522
+ continue;
523
+ }
524
+ if (value?.type === "citeark.completed_result_reused") {
525
+ events.push({ ...base, kind: "checkpoint", level: "success", message: "恢复点已经包含完整结果,跳过重复实验并直接回传" });
526
+ continue;
527
+ }
528
+ if (value?.type === "citeark.result_ready_agent_stop_requested") {
529
+ events.push({ ...base, kind: "agent", level: "info", message: "结果已经稳定,正在自动结束 Agent 进程" });
530
+ continue;
531
+ }
532
+ if (value?.type === "citeark.result_ready_agent_force_stopped") {
533
+ events.push({ ...base, kind: "agent", level: "warning", message: "Agent 未在宽限期内退出,已由执行器强制结束" });
534
+ continue;
535
+ }
536
+ if (value?.type === "citeark.agent_stopped_after_result_ready") {
537
+ events.push({ ...base, kind: "agent", level: "success", message: "Agent 已在完整结果生成后自动收尾" });
538
+ continue;
539
+ }
540
+ if (value?.type === "thread.started") {
541
+ events.push({ ...base, kind: "agent", level: "info", message: "Agent 会话已经建立" });
542
+ continue;
543
+ }
544
+ if (value?.type === "turn.started") {
545
+ events.push({ ...base, kind: "agent", level: "info", message: "Agent 开始新的工作回合" });
546
+ continue;
547
+ }
548
+ if (value?.type === "turn.completed") {
549
+ events.push({ ...base, kind: "agent", level: "success", message: "Agent 工作回合完成" });
550
+ continue;
551
+ }
552
+ if (value?.type === "item.started" && value.item?.type === "command_execution") {
553
+ events.push({ ...base, kind: "command", level: "info", message: classifyCommand(value.item.command) });
554
+ continue;
555
+ }
556
+ if (value?.type === "item.completed" && value.item?.type === "command_execution" && Number(value.item.exit_code) !== 0) {
557
+ events.push({ ...base, kind: "command", level: "warning", message: `命令执行异常,退出码 ${value.item.exit_code}` });
558
+ continue;
559
+ }
560
+ if (value?.type === "item.completed" && value.item?.type === "agent_message" && value.item.text) {
561
+ events.push({ ...base, kind: "agent-message", level: "info", message: `Agent 更新:${cleanPublicText(value.item.text, 220)}` });
562
+ continue;
563
+ }
564
+ if (value?.type === "tool_progress") {
565
+ events.push({
566
+ ...base,
567
+ kind: "tool-heartbeat",
568
+ level: "info",
569
+ message: `${toolLabel(value.tool_name)} 已持续运行 ${Math.max(0, Number(value.elapsed_time_seconds) || 0)} 秒`,
570
+ });
571
+ continue;
572
+ }
573
+ if (value?.type === "assistant" && Array.isArray(value.message?.content)) {
574
+ for (const content of value.message.content) {
575
+ if (content?.type === "tool_use") {
576
+ events.push({ ...base, kind: "tool", level: "info", message: summarizeToolUse(content) });
577
+ } else if (content?.type === "text" && content.text) {
578
+ events.push({ ...base, kind: "agent-message", level: "info", message: `Agent 更新:${cleanPublicText(content.text, 220)}` });
579
+ }
580
+ }
581
+ continue;
582
+ }
583
+ if (value?.type === "result") {
584
+ events.push({
585
+ ...base,
586
+ kind: "agent",
587
+ level: value.is_error ? "error" : "success",
588
+ message: value.is_error ? "Agent 回合返回错误" : "Agent 已提交本轮结果",
589
+ });
590
+ }
591
+ }
592
+ return events.slice(-24);
593
+ }
594
+
595
+ function summarizeToolUse(content) {
596
+ const name = toolLabel(content.name);
597
+ if (content.name === "Bash") return classifyCommand(content.input?.command, content.input?.description);
598
+ if (content.name === "Read") return `正在读取文件:${cleanPublicText(content.input?.file_path ?? "仓库文件", 120)}`;
599
+ if (content.name === "Write" || content.name === "Edit") return `正在更新文件:${cleanPublicText(content.input?.file_path ?? "实验文件", 120)}`;
600
+ return `正在调用工具:${name}`;
601
+ }
602
+
603
+ function classifyCommand(command, description) {
604
+ if (description) return cleanPublicText(description, 180);
605
+ const raw = cleanPublicText(command ?? "", 180);
606
+ const lower = raw.toLowerCase();
607
+ if (/\b(pip|npm|apt-get|conda|uv)\b.*\binstall\b/.test(lower)) return "正在安装实验依赖";
608
+ if (/\b(curl|wget|download|huggingface|git lfs)\b/.test(lower)) return "正在下载数据、模型或实验资源";
609
+ if (/\b(train\.py|python[^;]*(train|finetune)|torchrun)\b/.test(lower)) return "正在运行模型训练";
610
+ if (/\b(evaluation\.py|evaluate|pytest|unittest|benchmark)\b/.test(lower)) return "正在运行论文评测或测试";
611
+ if (/\bgit\b/.test(lower)) return "正在检查代码仓库与变更";
612
+ if (/\b(ps|tail|top|nvidia-smi)\b/.test(lower)) return "正在检查进程、资源或运行日志";
613
+ return raw ? `正在执行命令:${raw}` : "正在执行实验命令";
614
+ }
615
+
616
+ function classifyActivityHealth({ status, generatedAt, lastHeartbeatAt, lastActivityAt, lastOutputAt, hasMonitor }) {
617
+ if (status !== "running") {
618
+ return {
619
+ code: status === "failed" ? "failed" : status === "prepared" ? "waiting" : "finished",
620
+ label: status === "failed" ? "运行失败" : status === "prepared" ? "等待执行" : status === "cancelled" ? "已取消" : "已结束",
621
+ attentionRequired: false,
622
+ controllerAlive: false,
623
+ reason: status === "failed"
624
+ ? "流水线已经记录失败原因"
625
+ : status === "prepared"
626
+ ? "运行包已经准备,但尚未真实执行"
627
+ : status === "cancelled"
628
+ ? "流水线已按用户请求停止"
629
+ : "流水线已经进入终态",
630
+ };
631
+ }
632
+ const heartbeatAge = ageMs(generatedAt, lastHeartbeatAt);
633
+ const outputAge = ageMs(generatedAt, lastOutputAt);
634
+ const activityAge = ageMs(generatedAt, lastActivityAt);
635
+ if (hasMonitor) {
636
+ if (heartbeatAge === null || heartbeatAge > HEARTBEAT_STALE_MS) {
637
+ return { code: "stalled", label: "疑似中断", attentionRequired: true, controllerAlive: false, reason: "控制器心跳已超过 2 分钟没有更新" };
638
+ }
639
+ if (heartbeatAge > HEARTBEAT_HEALTHY_MS) {
640
+ return { code: "delayed", label: "心跳延迟", attentionRequired: false, controllerAlive: true, reason: "控制器仍可能在线,但心跳刷新出现延迟" };
641
+ }
642
+ if (outputAge !== null && outputAge > OUTPUT_ATTENTION_MS) {
643
+ return { code: "quiet", label: "长时间无输出", attentionRequired: true, controllerAlive: true, reason: "控制器心跳正常,但超过 15 分钟没有新的 Agent 输出或日志" };
644
+ }
645
+ if (outputAge !== null && outputAge > OUTPUT_QUIET_MS) {
646
+ return { code: "quiet", label: "计算中", attentionRequired: false, controllerAlive: true, reason: "控制器心跳正常,当前步骤暂时没有新输出" };
647
+ }
648
+ return { code: "active", label: "持续运行", attentionRequired: false, controllerAlive: true, reason: "控制器心跳和执行活动均正常" };
649
+ }
650
+ return { code: "stalled", label: "缺少控制器心跳", attentionRequired: true, controllerAlive: false, reason: "CAP 2.0 流水线缺少必需的控制器心跳记录" };
651
+ }
652
+
653
+ function latestTimestamp(...values) {
654
+ return values
655
+ .map(validTimestamp)
656
+ .filter(Boolean)
657
+ .sort((left, right) => Date.parse(right) - Date.parse(left))[0] ?? null;
658
+ }
659
+
660
+ function validTimestamp(value) {
661
+ if (!value) return null;
662
+ const parsed = Date.parse(value);
663
+ return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
664
+ }
665
+
666
+ function ageMs(now, then) {
667
+ if (!then) return null;
668
+ const value = Date.parse(now) - Date.parse(then);
669
+ return Number.isFinite(value) ? Math.max(0, value) : null;
670
+ }
671
+
672
+ function eventSortTime(event) {
673
+ return Date.parse(event?.at ?? event?.observedAt ?? 0) || 0;
674
+ }
675
+
676
+ function uniqueEvent() {
677
+ const seen = new Set();
678
+ return (event) => {
679
+ const key = `${event.source}|${event.at ?? event.observedAt}|${event.kind}|${event.message}`;
680
+ if (seen.has(key)) return false;
681
+ seen.add(key);
682
+ return true;
683
+ };
684
+ }
685
+
686
+ function toolLabel(value) {
687
+ return ({ Bash: "命令", Read: "文件读取", Write: "文件写入", Edit: "文件编辑", TaskOutput: "后台任务" })[value] ?? value ?? "Agent 工具";
688
+ }
689
+
690
+ function cleanPublicText(value, maximumLength) {
691
+ const normalized = String(value ?? "")
692
+ .replaceAll(/sk-[A-Za-z0-9_-]{12,}/g, "<已隐藏密钥>")
693
+ .replaceAll(/((?:api[_-]?key|token|password|secret)\s*[=:]\s*)[^\s,;]+/gi, "$1<已隐藏>")
694
+ .replaceAll(/\s+/g, " ")
695
+ .trim();
696
+ return normalized.length > maximumLength ? `${normalized.slice(0, maximumLength - 1)}…` : normalized;
697
+ }
698
+
699
+ async function safeJson(target) {
700
+ if (!target || typeof target !== "string") return null;
701
+ try {
702
+ return JSON.parse(await readFile(target, "utf8"));
703
+ } catch {
704
+ return null;
705
+ }
706
+ }
707
+
708
+ async function firstJson(targets) {
709
+ for (const target of targets) {
710
+ const value = await safeJson(target);
711
+ if (value) return value;
712
+ }
713
+ return null;
714
+ }
715
+
716
+ function executionAttemptRows(stage, run) {
717
+ const completed = (run?.attempts ?? []).map((attempt) => ({
718
+ stage,
719
+ attempt: attempt.attempt,
720
+ startedAt: attempt.startedAt,
721
+ finishedAt: attempt.finishedAt,
722
+ durationMs: validDuration(attempt.startedAt, attempt.finishedAt),
723
+ exitCode: attempt.exitCode,
724
+ timedOut: attempt.timedOut,
725
+ executor: attempt.executor ?? null,
726
+ batchJob: attempt.batchJob ?? null,
727
+ batchState: attempt.batchState ?? null,
728
+ infrastructureAttempt: attempt.infrastructureAttempt ?? null,
729
+ infrastructureAttemptLimit: attempt.infrastructureAttemptLimit ?? null,
730
+ resumeCount: attempt.resumeCount ?? 0,
731
+ checkpoint: publicCheckpoint(attempt.checkpoint),
732
+ }));
733
+ const recordedJobs = new Set(completed.map((attempt) => attempt.batchJob).filter(Boolean));
734
+ const live = (run?.cloudExecution?.jobs ?? [])
735
+ .filter((job) => !recordedJobs.has(job.name))
736
+ .map((job) => ({
737
+ stage,
738
+ attempt: job.attempt,
739
+ startedAt: job.startedAt,
740
+ finishedAt: job.finishedAt ?? null,
741
+ durationMs: validDuration(job.startedAt, job.finishedAt ?? new Date().toISOString()),
742
+ exitCode: null,
743
+ timedOut: job.state === "TIMED_OUT",
744
+ executor: "gcp-batch",
745
+ batchJob: job.name,
746
+ batchState: job.state,
747
+ infrastructureAttempt: job.infrastructureAttempt ?? 1,
748
+ infrastructureAttemptLimit: job.infrastructureAttemptLimit ?? null,
749
+ resumeCount: job.resumeCount ?? 0,
750
+ checkpoint: publicCheckpoint(job.checkpoint),
751
+ }));
752
+ return [...completed, ...live];
753
+ }
754
+
755
+ function cloudResilience(cloud) {
756
+ if (!cloud) return null;
757
+ const jobs = cloud.jobs ?? [];
758
+ const current = jobs[jobs.length - 1] ?? null;
759
+ const currentCounters = infrastructureCounters(current);
760
+ return {
761
+ enabled: cloud.checkpointing?.enabled === true,
762
+ criticalIntervalSeconds: cloud.checkpointing?.criticalIntervalSeconds ?? null,
763
+ fullIntervalSeconds: cloud.checkpointing?.fullIntervalSeconds ?? null,
764
+ infrastructureAttempt: currentCounters.infrastructureAttempt,
765
+ infrastructureAttemptLimit: current?.infrastructureAttemptLimit ?? null,
766
+ resumeCount: jobs.reduce((total, job) => total + infrastructureCounters(job).resumeCount, 0),
767
+ interruptionCount: jobs.reduce((total, job) => total + infrastructureCounters(job).interruptionCount, 0),
768
+ latestCheckpoint: publicCheckpoint(current?.checkpoint),
769
+ currentJobState: current?.state ?? null,
770
+ };
771
+ }
772
+
773
+ function publicCloudView(cloud) {
774
+ if (!cloud) return null;
775
+ return {
776
+ ...structuredClone(cloud),
777
+ jobs: (cloud.jobs ?? []).map((job) => {
778
+ const counters = infrastructureCounters(job);
779
+ return {
780
+ ...structuredClone(job),
781
+ ...counters,
782
+ checkpoint: publicCheckpoint(job.checkpoint),
783
+ activity: job.activity ? {
784
+ traceDigest: job.activity.traceDigest ?? null,
785
+ lastOutputAt: validTimestamp(job.activity.lastOutputAt),
786
+ eventCount: Array.isArray(job.activity.traceTail) ? job.activity.traceTail.length : 0,
787
+ } : null,
788
+ };
789
+ }),
790
+ };
791
+ }
792
+
793
+ function publicCheckpoint(checkpoint) {
794
+ if (!checkpoint) return null;
795
+ const { activity: _privateActivity, ...safe } = structuredClone(checkpoint);
796
+ return safe;
797
+ }
798
+
799
+ function infrastructureCounters(job) {
800
+ if (!job) return { infrastructureAttempt: null, resumeCount: 0, interruptionCount: 0 };
801
+ const checkpointRetry = Number(job.checkpoint?.infrastructureRetry);
802
+ const resumeCount = Number.isInteger(checkpointRetry) && checkpointRetry >= 0
803
+ ? checkpointRetry
804
+ : Math.max(0, Number(job.resumeCount ?? 0));
805
+ return {
806
+ infrastructureAttempt: resumeCount + 1,
807
+ resumeCount,
808
+ interruptionCount: resumeCount,
809
+ };
810
+ }
811
+
812
+ function firstModel(usage) {
813
+ return Object.keys(usage?.models ?? {})[0] ?? null;
814
+ }
815
+
816
+ function validDuration(start, end) {
817
+ const value = Date.parse(end) - Date.parse(start);
818
+ return Number.isFinite(value) && value >= 0 ? value : null;
819
+ }
820
+
821
+ function summarizePipelines(pipelines) {
822
+ const totals = emptyTotals();
823
+ totals.runs = pipelines.length;
824
+ totals.running = pipelines.filter((item) => item.status === "running").length;
825
+ totals.runningActive = pipelines.filter((item) => item.status === "running" && item.activity?.health === "active").length;
826
+ totals.attentionRequired = pipelines.filter((item) => item.activity?.attentionRequired).length;
827
+ totals.stalled = pipelines.filter((item) => item.status === "running" && item.activity?.health === "stalled").length;
828
+ totals.completed = pipelines.filter((item) => item.status === "completed").length;
829
+ totals.failed = pipelines.filter((item) => item.status === "failed").length;
830
+ totals.reproduced = pipelines.filter((item) => item.verificationStatus === "reproduced").length;
831
+ totals.approximatelyReproduced = pipelines.filter((item) => item.verificationStatus === "approximately_reproduced").length;
832
+ totals.notReproduced = pipelines.filter((item) => item.verificationStatus === "not_reproduced").length;
833
+ totals.inconclusive = pipelines.filter((item) => item.verificationStatus === "inconclusive").length;
834
+ totals.assessed = totals.reproduced + totals.approximatelyReproduced + totals.notReproduced + totals.inconclusive;
835
+ totals.decisive = totals.reproduced + totals.approximatelyReproduced + totals.notReproduced;
836
+ totals.decisiveRate = totals.assessed
837
+ ? Math.round((totals.decisive / totals.assessed) * 100)
838
+ : 0;
839
+ totals.durationMs = pipelines.reduce((sum, item) => sum + Number(item.durationMs ?? 0), 0);
840
+ totals.usage = mergeAgentUsage(...pipelines.map((item) => item.usage));
841
+ const availableUsageCount = pipelines.filter((item) => item.usage?.availability?.status === "reported").length;
842
+ totals.usage.availability = {
843
+ status: availableUsageCount === 0 ? "unavailable" : availableUsageCount === pipelines.length ? "reported" : "partial",
844
+ reportedRuns: availableUsageCount,
845
+ totalRuns: pipelines.length,
846
+ };
847
+ totals.usage.pricing = pipelines.every((item) =>
848
+ String(item.agent?.model ?? "").toLowerCase().includes("deepseek"),
849
+ )
850
+ ? estimateDeepSeekCost(totals.usage)
851
+ : pricingFor(totals.usage, null);
852
+ return totals;
853
+ }
854
+
855
+ function pricingFor(usage, model) {
856
+ if (String(model ?? "").toLowerCase().includes("deepseek")) {
857
+ return estimateDeepSeekCost(usage, { defaultModel: model });
858
+ }
859
+ if (usage?.costStatus === "reported" || usage?.costStatus === "partial") {
860
+ return {
861
+ currency: "USD",
862
+ source: usage.costStatus === "reported" ? "agent-reported" : "agent-reported-partial",
863
+ costUsd: Number(usage.costUsd),
864
+ };
865
+ }
866
+ return { currency: null, source: "unavailable" };
867
+ }
868
+
869
+ function usageAvailability(usage) {
870
+ const reported = Number(usage?.totalTokens ?? 0) > 0
871
+ || Number(usage?.turns ?? 0) > 0
872
+ || Number(usage?.costUsd ?? 0) > 0
873
+ || Object.keys(usage?.models ?? {}).length > 0;
874
+ return reported
875
+ ? { status: "reported", reason: null }
876
+ : { status: "unavailable", reason: "当前 Agent CLI trace 未提供 Token 用量字段" };
877
+ }
878
+
879
+ function emptyTotals() {
880
+ return {
881
+ runs: 0,
882
+ running: 0,
883
+ runningActive: 0,
884
+ attentionRequired: 0,
885
+ stalled: 0,
886
+ completed: 0,
887
+ failed: 0,
888
+ reproduced: 0,
889
+ approximatelyReproduced: 0,
890
+ notReproduced: 0,
891
+ inconclusive: 0,
892
+ assessed: 0,
893
+ decisive: 0,
894
+ decisiveRate: 0,
895
+ durationMs: 0,
896
+ usage: mergeAgentUsage(),
897
+ };
898
+ }