Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

## Table of Contents

- [What's new (2026-06-21) — W3C Baggage Propagation](#whats-new-2026-06-21--w3c-baggage-propagation)
- [What's new (2026-06-21) — Dataset Diff (Row-Set Change Report)](#whats-new-2026-06-21--dataset-diff-row-set-change-report)
- [What's new (2026-06-21) — Distribution Drift Detection](#whats-new-2026-06-21--distribution-drift-detection)
- [What's new (2026-06-21) — Layered Configuration Resolver](#whats-new-2026-06-21--layered-configuration-resolver)
Expand Down Expand Up @@ -136,6 +137,12 @@

---

## What's new (2026-06-21) — W3C Baggage Propagation

Carry cross-cutting key-value context across HTTP. Full reference: [`docs/source/Eng/doc/new_features/v84_features_doc.rst`](docs/source/Eng/doc/new_features/v84_features_doc.rst).

- **`Baggage` / `parse_baggage` / `format_baggage` / `inject_baggage` / `extract_baggage`** (`AC_baggage_parse`, `AC_baggage_format`): `trace_context` carried trace/span identity but nothing propagated cross-cutting context (`run_id`/`tenant`/`experiment`). This implements the W3C Baggage header — a percent-encoded `key=value` list — with an immutable `Baggage` (set/remove return new instances) and case-insensitive inject/extract over a headers dict. Pairs with `trace_context`. Pure-stdlib, deterministic.

## What's new (2026-06-21) — Dataset Diff (Row-Set Change Report)

Diff two tabular extracts by key. Full reference: [`docs/source/Eng/doc/new_features/v83_features_doc.rst`](docs/source/Eng/doc/new_features/v83_features_doc.rst).
Expand Down
7 changes: 7 additions & 0 deletions README/README_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

## 目录

- [本次更新 (2026-06-21) — W3C Baggage 传播](#本次更新-2026-06-21--w3c-baggage-传播)
- [本次更新 (2026-06-21) — 数据集差异(数据行变更报告)](#本次更新-2026-06-21--数据集差异数据行变更报告)
- [本次更新 (2026-06-21) — 分布漂移检测](#本次更新-2026-06-21--分布漂移检测)
- [本次更新 (2026-06-21) — 分层配置解析器](#本次更新-2026-06-21--分层配置解析器)
Expand Down Expand Up @@ -135,6 +136,12 @@

---

## 本次更新 (2026-06-21) — W3C Baggage 传播

跨 HTTP 携带横切键值上下文。完整参考:[`docs/source/Zh/doc/new_features/v84_features_doc.rst`](../docs/source/Zh/doc/new_features/v84_features_doc.rst)。

- **`Baggage` / `parse_baggage` / `format_baggage` / `inject_baggage` / `extract_baggage`**(`AC_baggage_parse`、`AC_baggage_format`):`trace_context` 携带 trace/span 身份,但没有东西传播横切上下文(`run_id`/`tenant`/`experiment`)。本功能实现 W3C Baggage 标头 —— percent-encoded 的 `key=value` 列表 —— 以不可变的 `Baggage`(set/remove 返回新实例)与不分大小写的 inject/extract。与 `trace_context` 搭配。纯标准库、确定。

## 本次更新 (2026-06-21) — 数据集差异(数据行变更报告)

按键比对两份表格式提取。完整参考:[`docs/source/Zh/doc/new_features/v83_features_doc.rst`](../docs/source/Zh/doc/new_features/v83_features_doc.rst)。
Expand Down
7 changes: 7 additions & 0 deletions README/README_zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

## 目錄

- [本次更新 (2026-06-21) — W3C Baggage 傳播](#本次更新-2026-06-21--w3c-baggage-傳播)
- [本次更新 (2026-06-21) — 資料集差異(資料列變更報告)](#本次更新-2026-06-21--資料集差異資料列變更報告)
- [本次更新 (2026-06-21) — 分布漂移偵測](#本次更新-2026-06-21--分布漂移偵測)
- [本次更新 (2026-06-21) — 分層設定解析器](#本次更新-2026-06-21--分層設定解析器)
Expand Down Expand Up @@ -135,6 +136,12 @@

---

## 本次更新 (2026-06-21) — W3C Baggage 傳播

跨 HTTP 攜帶橫切鍵值脈絡。完整參考:[`docs/source/Zh/doc/new_features/v84_features_doc.rst`](../docs/source/Zh/doc/new_features/v84_features_doc.rst)。

- **`Baggage` / `parse_baggage` / `format_baggage` / `inject_baggage` / `extract_baggage`**(`AC_baggage_parse`、`AC_baggage_format`):`trace_context` 攜帶 trace/span 身分,但沒有東西傳播橫切脈絡(`run_id`/`tenant`/`experiment`)。本功能實作 W3C Baggage 標頭 —— percent-encoded 的 `key=value` 清單 —— 以不可變的 `Baggage`(set/remove 回傳新實例)與不分大小寫的 inject/extract。與 `trace_context` 搭配。純標準函式庫、具決定性。

## 本次更新 (2026-06-21) — 資料集差異(資料列變更報告)

依鍵比對兩份表格式萃取。完整參考:[`docs/source/Zh/doc/new_features/v83_features_doc.rst`](../docs/source/Zh/doc/new_features/v83_features_doc.rst)。
Expand Down
41 changes: 41 additions & 0 deletions docs/source/Eng/doc/new_features/v84_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
W3C Baggage Propagation
=======================

``trace_context`` carries trace and span identity across an HTTP boundary, but
there was no way to propagate cross-cutting key-value context (``run_id`` /
``tenant`` / ``experiment``) alongside it. This implements the W3C Baggage
header — a percent-encoded ``key=value`` list — so a run can attach such context
to outgoing requests and read it back on the other side.

Pure standard library (``urllib.parse``); imports no ``PySide6``. ``Baggage`` is
immutable (mutators return a new instance), so propagation is deterministic.

Headless API
------------

.. code-block:: python

from je_auto_control import Baggage, inject_baggage, extract_baggage

bag = Baggage({"tenant": "acme"}).set("run_id", "42")
headers = inject_baggage(outgoing_headers, bag)
# headers["baggage"] == "tenant=acme,run_id=42"

received = extract_baggage(request_headers)
tenant = received.get("tenant")

``Baggage`` wraps an immutable key-value map: ``get`` reads, ``set`` / ``remove``
return new instances, and ``to_dict`` exports the entries. ``parse_baggage``
reads the header (dropping optional ``;metadata`` and rejecting empty keys),
``format_baggage`` percent-encodes keys and values back into a header value,
and ``inject_baggage`` / ``extract_baggage`` write and read the ``baggage``
header on a request dict (extraction is case-insensitive). Pairs naturally with
``trace_context`` to carry context alongside the trace.

Executor commands
-----------------

``AC_baggage_parse`` parses a ``header`` into ``{items}``; ``AC_baggage_format``
serialises an ``items`` object into ``{header}``. Both are exposed as MCP tools
(``ac_baggage_parse`` / ``ac_baggage_format``) and as Script Builder commands
under **Data**.
1 change: 1 addition & 0 deletions docs/source/Eng/eng_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ Comprehensive guides for all AutoControl features.
doc/new_features/v81_features_doc
doc/new_features/v82_features_doc
doc/new_features/v83_features_doc
doc/new_features/v84_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
35 changes: 35 additions & 0 deletions docs/source/Zh/doc/new_features/v84_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
W3C Baggage 傳播
===============

``trace_context`` 能跨 HTTP 邊界攜帶 trace 與 span 身分,但沒有辦法在旁邊一併傳播橫切的鍵值脈絡
(``run_id`` / ``tenant`` / ``experiment``)。本功能實作 W3C Baggage 標頭 —— 一個 percent-encoded 的
``key=value`` 清單 —— 讓一次執行能把這類脈絡附加到外送請求,並在另一端讀回。

純標準函式庫(``urllib.parse``);不匯入 ``PySide6``。``Baggage`` 不可變(變更操作回傳新實例),因此
傳播具決定性。

無頭 API
--------

.. code-block:: python

from je_auto_control import Baggage, inject_baggage, extract_baggage

bag = Baggage({"tenant": "acme"}).set("run_id", "42")
headers = inject_baggage(outgoing_headers, bag)
# headers["baggage"] == "tenant=acme,run_id=42"

received = extract_baggage(request_headers)
tenant = received.get("tenant")

``Baggage`` 包裝一個不可變的鍵值對應:``get`` 讀取,``set`` / ``remove`` 回傳新實例,``to_dict`` 匯出
條目。``parse_baggage`` 解析標頭(去除選用的 ``;metadata`` 並拒絕空鍵),``format_baggage`` 將鍵與值
percent-encode 回標頭值,``inject_baggage`` / ``extract_baggage`` 在請求 dict 上寫入與讀取 ``baggage``
標頭(讀取不分大小寫)。與 ``trace_context`` 自然搭配,在 trace 之外攜帶脈絡。

執行器命令
----------

``AC_baggage_parse`` 把 ``header`` 解析成 ``{items}``;``AC_baggage_format`` 把 ``items`` 物件序列化成
``{header}``。兩者皆以 MCP 工具(``ac_baggage_parse`` / ``ac_baggage_format``)以及 Script Builder 中
**Data** 分類下的命令提供。
1 change: 1 addition & 0 deletions docs/source/Zh/zh_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ AutoControl 所有功能的完整使用指南。
doc/new_features/v81_features_doc
doc/new_features/v82_features_doc
doc/new_features/v83_features_doc
doc/new_features/v84_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
6 changes: 6 additions & 0 deletions je_auto_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,10 @@
format_traceparent, format_tracestate, inject_context, new_root_context,
new_span_id, new_trace_id, parse_traceparent, parse_tracestate,
)
# W3C Baggage propagation (cross-cutting key-value context)
from je_auto_control.utils.baggage import (
Baggage, extract_baggage, format_baggage, inject_baggage, parse_baggage,
)
# Background popup/interrupt watchdog (unattended automation)
from je_auto_control.utils.watchdog import (
PopupWatchdog, WatchdogRule, default_popup_watchdog,
Expand Down Expand Up @@ -922,6 +926,8 @@ def start_autocontrol_gui(*args, **kwargs):
"format_traceparent", "format_tracestate", "inject_context",
"new_root_context", "new_span_id", "new_trace_id", "parse_traceparent",
"parse_tracestate",
"Baggage", "extract_baggage", "format_baggage", "inject_baggage",
"parse_baggage",
# MCP server
"AuditLogger", "HttpMCPServer", "MCPContent", "MCPPrompt",
"MCPPromptArgument", "MCPResource", "MCPServer", "MCPTool",
Expand Down
16 changes: 16 additions & 0 deletions je_auto_control/gui/script_builder/command_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1624,6 +1624,22 @@ def _add_resilience_specs(specs: List[CommandSpec]) -> None:
),
description="Extract the W3C trace context from request headers.",
))
specs.append(CommandSpec(
"AC_baggage_parse", "Data", "Baggage: Parse Header",
fields=(
FieldSpec("header", FieldType.STRING,
placeholder="tenant=acme,run=42"),
),
description="Parse a W3C baggage header into key-value items.",
))
specs.append(CommandSpec(
"AC_baggage_format", "Data", "Baggage: Format Header",
fields=(
FieldSpec("items", FieldType.STRING,
placeholder='{"tenant": "acme", "run": "42"}'),
),
description="Serialise items into a percent-encoded baggage header.",
))
specs.append(CommandSpec(
"AC_profile_rows", "Data", "Data Profile: Profile Rows",
fields=(
Expand Down
9 changes: 9 additions & 0 deletions je_auto_control/utils/baggage/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""W3C Baggage propagation for AutoControl runs."""
from je_auto_control.utils.baggage.baggage import (
Baggage, extract_baggage, format_baggage, inject_baggage, parse_baggage,
)

__all__ = [
"Baggage", "extract_baggage", "format_baggage", "inject_baggage",
"parse_baggage",
]
102 changes: 102 additions & 0 deletions je_auto_control/utils/baggage/baggage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""W3C Baggage propagation (cross-cutting key-value context).

The just-shipped ``trace_context`` carries trace/span identity across an HTTP
boundary, but there was no way to propagate cross-cutting key-value context
(``run_id`` / ``tenant`` / ``experiment``) alongside it. This implements the
W3C Baggage header — a percent-encoded ``key=value`` list — so a run can attach
such context to outgoing requests and read it back on the other side.

Pure standard library (``urllib.parse``); imports no ``PySide6``. ``Baggage`` is
immutable (mutators return a new instance), so propagation is deterministic in
CI.
"""
from typing import Dict, List, Mapping, Optional, Tuple
from urllib.parse import quote, unquote

_MAX_ENTRIES = 180


class Baggage:
"""An immutable set of W3C baggage key-value entries."""

def __init__(self, items: Optional[Mapping[str, str]] = None) -> None:
self._items: Dict[str, str] = {str(key): str(value)
for key, value in (items or {}).items()}

def to_dict(self) -> Dict[str, str]:
"""Return the entries as a plain dict."""
return dict(self._items)

def get(self, key: str, default: Optional[str] = None) -> Optional[str]:
"""Return the value for ``key`` or ``default``."""
return self._items.get(key, default)

def set(self, key: str, value: str) -> "Baggage":
"""Return a new baggage with ``key`` set to ``value``."""
updated = dict(self._items)
updated[str(key)] = str(value)
return Baggage(updated)

def remove(self, key: str) -> "Baggage":
"""Return a new baggage with ``key`` removed."""
updated = dict(self._items)
updated.pop(key, None)
return Baggage(updated)

def __len__(self) -> int:
return len(self._items)

def __contains__(self, key: object) -> bool:
return key in self._items

def __eq__(self, other: object) -> bool:
return isinstance(other, Baggage) and other._items == self._items

def __repr__(self) -> str:
return f"Baggage({self._items!r})"


def _parse_member(member: str) -> Optional[Tuple[str, str]]:
pair = member.split(";", 1)[0].strip() # drop optional ;metadata
if not pair:
return None
key, sep, value = pair.partition("=")
if not sep or not key.strip():
return None
return unquote(key.strip()), unquote(value.strip())


def parse_baggage(header: Optional[str]) -> Baggage:
"""Parse a W3C ``baggage`` header into a :class:`Baggage`."""
items: Dict[str, str] = {}
for member in (header or "").split(","):
parsed = _parse_member(member)
if parsed is not None:
items[parsed[0]] = parsed[1]
return Baggage(items)


def format_baggage(baggage: Baggage) -> str:
"""Serialise a :class:`Baggage` into a percent-encoded header value."""
parts: List[str] = []
for key, value in list(baggage.to_dict().items())[:_MAX_ENTRIES]:
parts.append(f"{quote(key, safe='')}={quote(value, safe='')}")
return ",".join(parts)


def inject_baggage(headers: Optional[Dict[str, str]],
baggage: Baggage) -> Dict[str, str]:
"""Return ``headers`` with the ``baggage`` header set (when non-empty)."""
out = dict(headers or {})
encoded = format_baggage(baggage)
if encoded:
out["baggage"] = encoded
return out


def extract_baggage(headers: Optional[Dict[str, str]]) -> Baggage:
"""Extract a :class:`Baggage` from request headers (case-insensitive)."""
for key, value in (headers or {}).items():
if str(key).lower() == "baggage":
return parse_baggage(value)
return Baggage()
17 changes: 17 additions & 0 deletions je_auto_control/utils/executor/action_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3000,6 +3000,21 @@ def _trace_extract(headers: Any) -> Dict[str, Any]:
return {"context": ctx.to_dict() if ctx is not None else None}


def _baggage_parse(header: str) -> Dict[str, Any]:
"""Adapter: parse a W3C baggage header into {items}."""
from je_auto_control.utils.baggage import parse_baggage
return {"items": parse_baggage(header).to_dict()}


def _baggage_format(items: Any) -> Dict[str, Any]:
"""Adapter: serialise an items dict into a W3C baggage {header}."""
import json
from je_auto_control.utils.baggage import Baggage, format_baggage
if isinstance(items, str):
items = json.loads(items)
return {"header": format_baggage(Baggage(items))}


def _profile_rows(rows: Any, columns: Any = None) -> Dict[str, Any]:
"""Adapter: profile a row-set into per-column statistics."""
import json
Expand Down Expand Up @@ -4203,6 +4218,8 @@ def __init__(self):
"AC_http_replay": _http_replay,
"AC_trace_inject": _trace_inject,
"AC_trace_extract": _trace_extract,
"AC_baggage_parse": _baggage_parse,
"AC_baggage_format": _baggage_format,
"AC_profile_rows": _profile_rows,
"AC_infer_schema": _infer_schema,
"AC_parse_problem": _parse_problem,
Expand Down
24 changes: 23 additions & 1 deletion je_auto_control/utils/mcp_server/tools/_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -3509,6 +3509,27 @@ def data_profile_tools() -> List[MCPTool]:
]


def baggage_tools() -> List[MCPTool]:
return [
MCPTool(
name="ac_baggage_parse",
description=("Parse a W3C 'baggage' header (percent-encoded "
"key=value list, optional ;metadata) into {items}."),
input_schema=schema({"header": {"type": "string"}}, ["header"]),
handler=h.baggage_parse,
annotations=READ_ONLY,
),
MCPTool(
name="ac_baggage_format",
description=("Serialise an 'items' object into a percent-encoded "
"W3C baggage {header}."),
input_schema=schema({"items": {"type": "object"}}, ["items"]),
handler=h.baggage_format,
annotations=READ_ONLY,
),
]


def trace_context_tools() -> List[MCPTool]:
return [
MCPTool(
Expand Down Expand Up @@ -5070,7 +5091,8 @@ def media_assert_tools() -> List[MCPTool]:
search_index_tools, stats_tools, recurrence_tools, text_diff_tools,
feature_flag_tools, provenance_tools, json_contract_tools, chaos_tools,
slo_tools, percentiles_tools, bulkhead_tools, http_cassette_tools,
trace_context_tools, data_profile_tools, http_problem_tools, dotenv_tools,
trace_context_tools, baggage_tools,
data_profile_tools, http_problem_tools, dotenv_tools,
sse_client_tools, layered_config_tools, data_drift_tools,
dataset_diff_tools,
saga_tools, decision_table_tools, locator_repair_tools,
Expand Down
10 changes: 10 additions & 0 deletions je_auto_control/utils/mcp_server/tools/_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1726,6 +1726,16 @@ def trace_extract(headers):
return _trace_extract(headers)


def baggage_parse(header):
from je_auto_control.utils.executor.action_executor import _baggage_parse
return _baggage_parse(header)


def baggage_format(items):
from je_auto_control.utils.executor.action_executor import _baggage_format
return _baggage_format(items)


def profile_rows(rows, columns=None):
from je_auto_control.utils.executor.action_executor import _profile_rows
return _profile_rows(rows, columns)
Expand Down
Loading
Loading