Repository navigation
Add trace schema v1, replay and dataset export #211
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # trace_store.py —— 规范 | ||
|
|
||
| <!-- verified-against: 2026-09-28 --> | ||
|
|
||
| `LOC ~260 · trace/1:Copilot 与 RB 共用的模型调用/判定/结果记录 · refactor-status: stable` | ||
|
|
||
| ## 职责 | ||
| - `TraceStore(root)`:追加式 `records/<日期>.jsonl`(事实来源)+ 内容寻址 blob | ||
| (`blobs/<aa>/<sha256>.gz`,gzip,读取时复核哈希)+ SQLite 索引 `index.db` | ||
| (按 kind、run、仓库、变更集、PR、规则 ID、角色、模型、结果查询;`rebuild_index` 可从 JSONL 重建)。 | ||
| - 记录(`schema: trace/1`):`kind` ∈ `model_call`/`decision`/`outcome`/`replay`;`context`(run_id、 | ||
| playbook、step、repo、changeset_id、pr、rule_ids…)、`model`(role、provider、model、effort、served_model)、 | ||
| `usage`、`seconds`、`inputs`/`outputs`(只存 blob 引用,从不内联正文)、`result`、`error`、`env` | ||
| (copilot 版本与 SHA)。`outcome` 是事后金标(merged、closed_unmerged、head_changed、rule_retired)。 | ||
| - `trace_context(**fields)`:用 contextvars 绑定上下文,嵌套叠加,线程/协程安全。 | ||
|
|
||
| ## 不变量 | ||
| - 写入前统一脱敏:已知令牌形态(GitHub、Anthropic、OpenAI、AWS、Slack、Bearer、私钥块)以及名字表明是 | ||
| 密钥的环境变量的值(路径形式的值除外)。脱敏发生在哈希之前,blob 与 JSONL 中都不会出现密钥。 | ||
| - 只依赖标准库;经 `sdk.v1` 导出(`TraceStore`、`TRACE_SCHEMA`、`trace_context`、`redact`)时不加载私有模块。 | ||
|
|
||
| ## 测试 | ||
| `test_trace_store.py`(schema 与去重 blob、索引与重建、脱敏、网关记录成功与失败调用、回放、导出的判定/结果 | ||
| 关联与校准集泄漏过滤、SDK 导出、知识服务的 decision/outcome 记录)。 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -104,29 +104,46 @@ def call_json(self, role: ModelRole, *, system: str, prompt: str, | |
| validate: Callable[[dict], None] | None = None) -> ModelReply: | ||
| transport = self._transport(role.provider) | ||
| started = time.time() | ||
| identity = {"role": role.name, "requested": role.label(), "provider": role.provider, | ||
| "model": role.model, "effort": role.effort} | ||
|
Comment on lines
+107
to
+108
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a configured provider CLI is missing or unauthenticated, Useful? React with 👍 / 👎. |
||
| try: | ||
| reply = transport.complete( | ||
| system=system, messages=[{"role": "user", "content": prompt}], | ||
| model=role.model, effort=role.effort, role=role.name) | ||
| except Exception as exc: # the transport's own failure modes | ||
| if self._recorder is not None: | ||
| self._recorder({**identity, "served_model": "", "stop_reason": "", "usage": {}, | ||
| "seconds": round(time.time() - started, 3), "system": system, | ||
| "prompt": prompt, "reply": "", "error": str(exc)[:2000]}) | ||
| raise ModelUnavailable(f"{role.label()} failed: {exc}") from exc | ||
| seconds = time.time() - started | ||
| text = "".join(getattr(block, "text", "") or "" for block in getattr(reply, "blocks", [])) | ||
| record = { | ||
| "role": role.name, "requested": role.label(), | ||
| **identity, | ||
| "served_model": getattr(reply, "model", "") or "", | ||
| "stop_reason": getattr(reply, "stop_reason", ""), | ||
| "usage": dict(getattr(reply, "usage", {}) or {}), | ||
| "seconds": round(seconds, 3), "system": system, "prompt": prompt, "reply": text, | ||
| } | ||
| if self._recorder is not None: | ||
| self._recorder(record) | ||
| # the record carries the call's FINAL verdict: a truncated, empty, | ||
| # unparseable or schema-failing reply is recorded as a failure, so it | ||
| # never becomes a training example | ||
| failure: ModelUnavailable | None = None | ||
| data: dict = {} | ||
| if getattr(reply, "stop_reason", "") == "max_tokens" or not text.strip(): | ||
| raise ModelUnavailable(f"{role.label()} timed out or returned nothing") | ||
| data = parse_json_object(text) | ||
| if validate is not None: | ||
| failure = ModelUnavailable(f"{role.label()} timed out or returned nothing") | ||
| else: | ||
| try: | ||
| validate(data) | ||
| except Exception as exc: # any malformed shape is a controlled refusal | ||
| raise ModelUnavailable(f"{role.label()} reply failed its schema: {exc!r}") from exc | ||
| data = parse_json_object(text) | ||
| if validate is not None: | ||
| try: | ||
| validate(data) | ||
| except Exception as exc: # any malformed shape is a controlled refusal | ||
| raise ModelUnavailable(f"{role.label()} reply failed its schema: {exc!r}") from exc | ||
| except ModelUnavailable as exc: | ||
| failure = exc | ||
| if self._recorder is not None: | ||
| self._recorder({**record, "error": str(failure) if failure else ""}) | ||
| if failure is not None: | ||
| raise failure | ||
| return ModelReply(role, data, text, record["served_model"], record["usage"], seconds) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When calibration is run through
infermatrix-copilot kb calibrate,cli.pypasses a plainModelGateway(Settings())with no recorder, so this context manager does not produce anymodel_callrecords. Consequently, all calibration model activity is absent from trace/1 despite the new every-call audit contract; construct the gateway with the state directory'sTraceStoreandtrace_recorderbefore invokingrun_calibration.Useful? React with 👍 / 👎.