Lifecycle (shared by every domain)
- RESOLVE — look up the DomainPack, Task and Adapter for a
RunSpec. - VALIDATE — parse and check the RunSpec, target, params and task config
(
core/validation.py). RESOLVE and VALIDATE failures areUserInputErrors: CLI exit code 2, no record written. - PREFLIGHT —
adapter.preflight(task): credentials, SDK, connectivity and the minimal permissions this benchmark needs on this cloud, before provisioning. A CRITICAL failure produces aninvalidrecord and never enters SETUP. Bypass with--skip-preflight; run standalone withcsbench doctor. - SETUP —
adapter.setup(): provision (Terraform) or connect (SDK/HTTP). - EXECUTE —
task.execute(adapter, params): drive the workload, return anObservationBundleof raw evidence only. - COLLECT —
core/observation.py::collect(): prove the bundle is well formed and canonically encodable. - SCORE —
task.score(bundle): a pure function producing measurements and findings. A scorer failure keeps the observations and records a SCORE error. - ENRICH — installed
ResultEnrichers, each isolated: a failure becomes an ENRICH stage error and never changesstatus. - PERSIST —
core/store.py: temp file, flush,fsync, atomic rename, plus an emergency dump into the system temp directory (path printed) when the results directory cannot be written. - PUBLISH — off unless a
ResultPublisheris injected. Runs after PERSIST and writes an append-only receipt; it can never rewrite the core record.
finally boundary
around SETUP → COLLECT: once SETUP is entered, adapter.teardown() always runs,
including when setup() failed half-way, and a teardown failure is recorded as
its own stage error without overwriting the execute or collect error that
caused it.
Result contract (schema 0.2)
Top-level fields are fixed:schema_version, run, identity, environment,
fingerprints, measurements, findings, observations, series,
artifacts, extensions, errors, status.
status∈completed·failed·invalid·unsupported. There is no booleanok, no top-levelmetrics, no top-levelevidence_layerand noconfig_hash.environment.execution∈simulated·live·unknownmarks whether the numbers came from a simulated runtime or a real cloud. It is folded into the environment fingerprint, so simulated and live results never pool together.- Every measurement carries
value,unitandevidence, optionallyaggregation,sample_countandnotes. - Every finding carries a stable
code, aseverity, asummary, itsevidenceanddetails. - Every stage error carries
stage,code,type,messageandretryable. Tracebacks are never stored in a record;csbench run --debugwrites them to<results>/debug/<run_id>.log. - All digests are full SHA-256 over the canonical JSON encoding
(
core/canonical.py): UTF-8, sorted keys, no insignificant whitespace, NaN/Infinity rejected. extensions["core"]is reserved for the core; a plugin writes under its own name (for exampleextensions["cb-pricing"]).
1.0 files are converted with
csbench migrate-results SOURCE --output DEST [--dry-run]: never in place,
never fabricating a fingerprint (unknown ones are the literal unknown), and
byte-identical on a repeat run. Each entry in migration-manifest.json records
the original path and its SHA-256.
Run plans and statistics (Phase 1C)
A single number is not a measurement.csbench run --repeat N --warmup W
(core/runplan.py) runs the same RunSpec warmup + repeat times through the
lifecycle above — every run is still its own digested 0.2 record — then:
- discards the
warmupruns (cold-start / JIT / cache effects are not the steady state you publish), tagging each record’s role underextensions["core"]["run_plan"]so the choice is auditable, never a fingerprint; - reduces the measured runs to one distribution per measurement
(
core/statistics.py): numeric →n / mean / stdev / min / max / p50 / p95 / cv, label → distribution /mode/agreement; - writes a
run_plan_aggregate(with its own SHA-256 digest) underresults/aggregates/…, which the report loader skips as a summary-of-records.
benchmark and environment fingerprints match. A plan whose benchmark or
environment changes mid-flight aggregates only the largest self-consistent
group and says so; csbench report flags a cell that mixes benchmarks (not
comparable at all) or implementation fingerprints (comparable only with the
caveat that the code changed). Only completed / unsupported runs contribute
numbers — a failed run is counted, but it has no verdict to pool.
Phase 1C does not itself ship plugin API ranges, JSON Schema, a conformance
kit or workload sandboxing — those are Phase 1D, which has since shipped in the
open core (plugin API version ranges + conflict detection, JSON Schema for
RunSpec / ResultRecord / manifest, csbench conformance, and workload
sandbox layers 1+2; see ROADMAP.md / CHANGELOG.md). Phase 1C aggregates only
scalar measurements, not time series.
Layers
Plugin contracts
Built-in and third-party (including closed-source commercial) packs load
identically — installing a package or dropping in a workload directory is enough.
A pack wiring a real cloud registers a
RuntimeProvider (and, optionally, a
CampaignProbeHook and ResourceReaper). The open core defines these
abstractions and now ships reference implementations for them — the live
Aliyun AgentRun and AWS AgentCore RuntimeProviders and ResourceReapers are
registered in-tree (pyproject.toml), so a real-cloud path is reproducible from
the open core alone. The seam stays open for additional third-party / commercial
providers to register the same way.
Control plane vs. data plane
Theexecute (touches the cloud) / score (pure) split is also the control
plane / data plane split. AgentRuntimeAdapter carries one extra seam,
run_data_plane_probe(name, params) -> ObservationBundle
(domains/agent_runtime/adapters/base.py): a latency-class “data-plane” task’s
execute() collapses to return adapter.run_data_plane_probe("<probe>", {...}),
so the adapter, not the task, decides where the measurement runs. The base
adapter dispatches to an in-process packer registry
(domains/agent_runtime/dataplane_dispatch.py), which is exactly what
local-sim and any not-yet-wired cloud use. A wired cloud can instead override
the seam to send the whole measurement to a load probe running inside the
target region, keeping the operator’s network and system proxy out of the
numbers, and return the same ObservationBundle for scoring. score() is
unchanged either way. The probe lifecycle for a run-plan campaign is driven by
the optional CampaignProbeHook (started, synced and reaped in a try/finally
around the task loop; default --probe local leaves this path untouched). The
open core defines the seam and the hook contract and ships the reference
Aliyun ECI probe carrier and its cloud transport in-tree; additional carriers
(other clouds, third-party or commercial) register the same way.
Evidence layers
A docs · B environment observation · C controlled measurement · D marketing.
Reports never blend dimensions into one score.
Current domains
agent-runtime— sessions, tool calling, fault recovery, observability, cost, isolation. All tasks run onlocal-sim; the full task list and adapter statuses are generated from the registry below (never hand-maintained — see “Maintaining this document”). It spans provisioning, runtime behaviour, tool registration, observability, cost and tenant isolation. Adapters use amanaged/transport/modesplit so the same code drives the local simulated runtime or, once a runtime-provider plugin is installed, a real cloud; latency-class tasks route through therun_data_plane_probeseam (see “Control plane vs. data plane”); capability probes raiseCapabilityNotSupported→ recorded as a finding, never a crash.- Reliability group — platform-as-agent-host stance. The unit under test is the platform as a host for a fixed agent, not the raw invoke API, so the reliability tasks measure whether the “platform + agent” combination recovers when a downstream tool genuinely fails or stalls. T1.3 (fault recovery) / T1.10 (retry storm) / T1.12 (head-of-line) inject faults/latency through the mock tool server’s
/fault/config+/latency/config(so they are platform-visible on the real agent→tool HTTP hop, authenticated with the mock token like every other call), give the benchmark agent a pinned retry contract (AGENT_RETRY_POLICYinagent_bundle/lc_agent.py: HTTP 5xx → retry twice, 200 ms backoff, 3 attempts total; 4xx / connection failures → no retry — part of the agent fingerprint, not a tunable knob), and observe by reading the mock’s own call counter (correlation-id bucketed so concurrent requests never cross-count) rather than inferring from the client loop. Each yields a three-state platform attribution — e.g. T1.3 →recoveredvsplatform_terminated(invoke cut before the agent could recover) vs fail-fast; T1.10 →storm_bounded_byagent / platform / none. T1.2 (state persistence) / T1.11 (concurrent writes) are honestly downgraded tounsupportedwith an evidence-A finding: FC-based AgentRun has no native session state, so a stateful platform legitimately scores a positive result here — a truthful differentiating negative, not a fabricated pass. bigdata-emr— minimal domain pack proving the abstraction generalizes: J1.1 wordcount smoke via the cross-language workload protocol. (This is a small task/adapter surface, not theskeletonAdapterStatusvalue — itslocal-processadapter isreference; only itsaws-emradapter isskeleton.)
Task & adapter inventory
agent-runtime — 27 tasks
Adapters:
aliyun-agentrun experimental · aws-agentcore skeleton · huawei-agentarts skeleton · local-sim reference · volcengine-agentkit skeleton
bigdata-emr — 1 task
Adapters:
aws-emr skeleton · local-process reference
0.2 Developer Preview readiness
referenceandwiredadapters can execute.experimentaladapters can execute with preview caveats.skeletonadapters are discoverable but rejected before preflight.- Current runnable references are
local-simandlocal-process. The Aliyun AgentRun (and AWS AgentCore)RuntimeProviders are registered in-tree. Thealiyun-agentrunadapter isexperimental: it runs end-to-end inmode: mockand its live path has run a full 27-task real-cloud campaign (cn-hangzhou, 2026-08-15 — 25completed+ 2 honestlyunsupported). It is not yet promoted towired(that status is reserved for a stabilized, repeatedly-validated live path); AWS AgentCore and the other clouds remainskeleton. - Bundled workloads live in
clousight_bench.resources.workloadsand are resolved withcore.resources.reference_workload_path(), so wheel and editable installs use the same files.
0.2 (see “Result contract” above); plugin
API 1.0 stays until its range-negotiation replacement in Phase 1D.
This repository is public and Apache-2.0 licensed; it contains the whole open
core, including the reproducibility mechanisms — the live Aliyun AgentRun
RuntimeProvider, ECI probe carrier, ResourceReaper, Terraform and the seed
pricing enricher. The moat is data and service, not withheld code. The
separate private clousight-bench-pro repository ships only:
cb-pricing— a fuller / fresher private price feed, consumed by the openpricingenricher via theCLOUSIGHT_PRICING_DATAenvironment variable (not an entry point);cb-dataservice— token-gated private / held-out datasets, registered as aPrivateAssetResolveron theclousight_bench.asset_resolversentry point, plus a managed object-store upload (placeholder);cb-adapters-enterprise— an empty placeholder for future managed-SaaS and private-cloud / 信创 adapters.
凭证与上手(便捷层)
原则:绝不让用户为 benchmark 单独造一套密钥,复用云自己的默认凭证链。core/credentials.py::resolve_credentials(target, platform)只探测凭证来源、从不读取/存储密钥值。解析顺序:auth_env(逃生口,显式指定 env 名)→profile(CLI 命名档)→ 标准 env 变量 → 凭证文件。返回CredentialResolution(ok, source, identity_hint, remediation),identity_hint只含变量名/档名,永不含密钥。ProviderAdapter.resolve_credentials()委托给上面的解析器(跨域,provider由target.provider或平台名前缀推断,如aliyun-agentrun→aliyun)。真 adapter 运行时仍交给官方 SDK 的默认链。- 各云凭证链登记在
PROVIDER_CREDENTIALS(aws / aliyun / huawei / volcengine:标准 env、profile env、凭证文件、SDK 模块、文档链接)。 - CLI:
csbench init <provider>生成私有*.local.yaml+.env.example并写.gitignore(配置无密钥);csbench doctor --config x分步体检(provider → SDK 可导入?→ 凭证链可解析?→mock_base_url可达?localhost 直接判失败),每步给可操作补救提示。
core/preflight.py 提供 Check / PreflightReport 与可复用校验函数(credential_check / sdk_check / mock_reachable_check),doctor 与 orchestrator 共用同一批函数(单一真源)。
ProviderAdapter.preflight() -> PreflightReport:默认校验凭证 + provider SDK。跨域可用。AgentRuntimeAdapter.preflight(task):super()之上,对真云平台(provider 非空)追加mock_base_url可达校验 +check_permissions(task)。local-sim(provider 为空、自托管 mock)不加这两项,永远通过。- orchestrator 在 RESOLVE 之后、SETUP 之前跑
adapter.preflight(task);任一 CRITICAL 失败即早退(不 provision),落一条ok=False、metrics.preflight_ok=False、error="preflight failed: ..."、notes=清单的记录。--skip-preflight关闭该 gate。
最小权限:(benchmark × 云) 矩阵映射
不同 benchmark 对不同云依赖的最小权限不同,因此权限校验按 task 映射,职责拆成两侧:- Task 声明 WHAT:
Task.required_permissions: tuple[str, ...]列出抽象能力令牌(与云无关)。令牌定义在domains/agent_runtime/permissions.py:session:create/session:state/tool:invoke/tool:register/trace:read/trace:export。当前五维声明:T1.2=session:create,session:state;T1.3=session:create,tool:invoke;T2.1=tool:register;T4.1=session:create,tool:invoke,trace:read;T4.2=session:create,tool:invoke,trace:export。 - Adapter 说 HOW + 验:每个真云 adapter 定义
PERMISSION_MAP(令牌 → 该云的具体最小 IAM/RAM 动作),required_actions(task)把 task 的令牌解析为该云动作集,_probe_permissions(actions)做真实鉴权(默认None=未验证 → WARNING 并列出「本次 benchmark 在该云所需的最小动作」;接真 adapter 覆写为 STS GetCallerIdentity / RAM policy 模拟 / dry-run,返回(ok, missing)→ 缺失即 CRITICAL 阻断)。
PERMISSION_MAP,互不侵入。csbench doctor --domain <d> --platform <p> --task <t> 直接打印该 (benchmark×云) 的最小动作清单。
测评集分发(三层,一套解析机制)
benchmark 不只是代码,还有资产(数据集、语料、held-out 判分键)。方法开源、数据分层、判分键私有——用同一套解析器承载,开放/私有边界只是配置:- manifest
assets:段(workload / task): core/assets.py:AssetSpec+resolve_asset(spec, base_dir, cache_dir, private_resolver);WorkloadEngine.resolve_assets()在 run 前解析全部资产,把{name: 本地路径}通过params["assets"]暴露给 workload。- 可复现不泄密:
describe()把资产identity()(name@version + sha256)折进 config_hash——只存指纹,不存内容;判分键内容永不入 record。 - 判分键抗污染:一个 task 的方法(怎么判)可开源,但其 held-out 键值可声明为
source: private——“怎么判”公开、“标准答案”私有,天然抗背题。 - 扩展点:私有解析器实现
PrivateAssetResolver(name+resolve(spec, cache_dir)),经clousight_bench.asset_resolvers注册;开源核心不带任何实现。clousight-bench-pro的cb-dataservice提供DataServiceAssetResolver(CLOUSIGHT_BENCH_TOKEN鉴权 + sha256 校验)。
数据契约与扩展点
ResultRecord 承载三条独立通道,互不覆盖,读者按需选取:
metrics(标量判分)— 报告层聚合、比较用的数字。series(时序,{name: [[t, value], ...]})— 高频采样,如逐秒延迟、GPU 利用率。artifacts({"kind", "path"/"uri", "media", "sha256"}指针)— OTel trace、日志包等大文件,record.json 里只存指纹,不存内容。
metric/log/result 并存、互不影响:
sample 事件按 series 名累积进 WorkloadResult.series;artifact 事件的 sha256 由引擎按 workload_dir/path 的文件内容计算,workload 本身不用算哈希。
落盘:record.json 保持历史路径 results/<domain>/<platform>/<task_id>-<run_id>.json 不变(core/store.py::ResultStore 负责,orchestrator._persist 委托给它,签名不变)。装了可选 [store] extra(duckdb + pyarrow)且记录带 series 时,series 会外置为该次 run 目录下的 series.parquet 长表,record.json 里的 series 字段改写为指针 {"$parquet": "<相对路径>"};未装 extra 时 series 原样内嵌在 record.json(小规模无损)。
Parquet 长表列(cb-dataservice / SaaS Web 端读取的稳定握手):
unit 列的取值约定:ResultStore 写 Parquet 时,对每个 series <name> 从 metrics["<name>__unit"] 读取其单位字符串(如 latency_ms 系列可在 metrics 里放 "latency_ms__unit": "ms");未提供则为空串 ""。这是一个隐式约定而非强制——不写也能落盘,只是 unit 列为空。
ResultStore.query_series(sql=None, glob="**/series.parquet") 用 DuckDB 直接对整个 results_dir 下的 Parquet 文件跑 SQL;glob 路径以参数绑定传入 read_parquet(?)(不做字符串拼接);缺 [store] extra 时抛 ImportError。
扩展点:clousight_bench.enrichers entry-point group 承载 ResultEnricher 子类——name: str + enrich(self, record: ResultRecord) -> ResultRecord。orchestrator.execute(spec, results_dir=None, enrich=True) 在构造完 record、_persist 之前,按 registry.load_enrichers() 返回的列表(按 name 排序,确定性执行顺序)依次调用。核心不带任何 enricher 实现(如成本预估)——商业插件通过 entry point 注入;CLI csbench run --no-enrich 可跳过整个链路。
插件兼容契约:clousight_bench.PLUGIN_API_VERSION = "1.0"(SemVer)。当 schema 字段、entry-point group 名、ResultEnricher/ResultStore 签名发生不兼容变更时才升 MAJOR。包版本与插件 API 版本是两个独立维度:clousight_bench.__version__(当前 0.2.0)随每次发布递增,PLUGIN_API_VERSION 只在插件面发生不兼容变更时才动,二者不同步演进。Phase 1A 的商业插件 pyproject.toml 按当前包版本 pin clousight-bench>=0.2,<0.3(>=1.0,<2.0 是早期设计文档里按 PLUGIN_API_VERSION 主版本设想的目标态写法,尚未随 0.2 开发者预览调整回真实包版本,此处更正)。
Pro 三个扩展边界(由 clousight-bench-pro 通过 entry point 注入;开源核心不携带任何实现):
Maintaining this document
Some of this document is generated, and the rest is prose you keep current by hand — in the same change that alters the behaviour.- Generated blocks live between
{/* BEGIN generated:… */}/{/* END generated:… */}markers (currently the task & adapter inventory). They are rendered from the domain registry — the same source ascsbench list --json— byscripts/gen_docs.py. Never edit inside the markers by hand. After adding/renaming a task or changing an adapter status, runpython scripts/gen_docs.pyand commit the result. CI runspython scripts/gen_docs.py --checkandtests/test_docs_inventory.py, so a stale block (a hand-typed “28 tasks” that no longer matches the registry) fails the build instead of shipping. - Prose sections (everything outside the markers) are part of the definition of done for a behaviour change: when a task’s measurement shape, an adapter’s contract, or a lifecycle rule changes, update the paragraph that describes it in the same PR. A doc that drifts a release behind is worse than no doc — reviewers stop trusting it. Keep each edit small and scoped to what changed; the generated blocks absorb the mechanical facts so the prose only has to carry intent.