Release 2026.10.04: payload size limit, dotted classifier keys
Tests / test (>=4,<5) (push) Canceled after 0s
Tests / test (==2.12.2) (push) Canceled after 0s
Tests / test (==2.14.3) (push) Canceled after 0s
Tests / test (>=3.3,<4) (push) Canceled after 0s
Tests / test-postgres (==2.12.2) (push) Canceled after 0s
Tests / test-postgres (>=4,<5) (push) Canceled after 0s
Tests / canary (push) Canceled after 0s
Tests / test (>=4,<5) (push) Canceled after 0s
Tests / test (==2.12.2) (push) Canceled after 0s
Tests / test (==2.14.3) (push) Canceled after 0s
Tests / test (>=3.3,<4) (push) Canceled after 0s
Tests / test-postgres (==2.12.2) (push) Canceled after 0s
Tests / test-postgres (>=4,<5) (push) Canceled after 0s
Tests / canary (push) Canceled after 0s
This commit is contained in:
@@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Calendar Versioning](https://calver.org/) (YYYY.MM.DD).
|
||||
|
||||
## [2026.10.04] - 2026-10-04
|
||||
|
||||
Bounded payloads, and classifier keys that reach into nested results.
|
||||
|
||||
### Added
|
||||
- `max_payload_bytes` on `InstrumentationMiddleware` and `instrument()`,
|
||||
64 KiB by default, `None` for no limit. Arguments and results stored in
|
||||
mode `full`, and every event's `attrs` (including `record_llm_call`'s),
|
||||
are bounded by it. A larger value is stored as
|
||||
`{"_truncated": true, "size": <bytes>, "preview": "<start of its JSON>"}`;
|
||||
the preview is 1000 characters (a quarter of the limit when smaller) and
|
||||
is taken after redaction. Call payloads reuse the size already measured
|
||||
for `args_size` and `result_size`, so the limit adds no encoding; the
|
||||
preview encodes only the start of the value. In `attrs`, only the largest
|
||||
values are replaced, so the dict keeps its keys. `EmbeddingSink` embeds a
|
||||
marker's preview. `fastmcp_feedback.instrumentation.payload` holds the
|
||||
marker helpers (`TRUNCATED`, `is_truncated`).
|
||||
- `message_keys` on `soft_error_classifier` (default `error`, `message`,
|
||||
`detail`, `reason`, as before).
|
||||
- Dotted paths in `message_keys` and `error_keys`: `"snapshot.error"` reads
|
||||
`result["snapshot"]["error"]`. A key that literally contains a dot is
|
||||
matched first; a missing step or a non-dict step counts as absent and
|
||||
never raises. Default outputs are unchanged.
|
||||
|
||||
### Changed
|
||||
- Stored payloads and event attrs over 64 KiB become markers. Earlier
|
||||
releases stored them whole; `max_payload_bytes=None` keeps that.
|
||||
|
||||
## [2026.10.02.1] - 2026-10-02
|
||||
|
||||
The identity resolver's second run is opt-in again for any resolver that
|
||||
|
||||
@@ -194,6 +194,42 @@ Set it with `mode=` or the `FEEDBACK_INSTRUMENTATION_MODE` environment variable.
|
||||
`meta_only_tools={"run_code", "create_token"}` keeps chosen tools at `meta`
|
||||
even in `full` mode.
|
||||
|
||||
### Payload size limit
|
||||
|
||||
`max_payload_bytes` (64 KiB by default, `None` for no limit) bounds what a
|
||||
row stores: the arguments and result in `full` mode, and every event's
|
||||
`attrs`. A larger value is stored as a marker instead:
|
||||
|
||||
```json
|
||||
{"_truncated": true, "size": 812345, "preview": "{\"frames\": [[0.1, 0.2], ..."}
|
||||
```
|
||||
|
||||
`size` is the value's JSON size in bytes, the same measurement as
|
||||
`args_size` and `result_size`, which the limit reuses, so it costs no second
|
||||
encoding. `preview` is the start of the JSON text, 1000 characters (a quarter
|
||||
of the limit when that is smaller), taken after redaction, so a secret cannot
|
||||
survive in it. For event `attrs` only the largest values are replaced, until
|
||||
the whole fits, so small fields such as `ok` keep their place:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from fastmcp_feedback.instrumentation import InstrumentationMiddleware, MemorySink
|
||||
|
||||
async def main():
|
||||
sink = MemorySink()
|
||||
mw = InstrumentationMiddleware([sink])
|
||||
mw.record_event("job.finished", key="j1", attrs={"ok": False, "error": "E" * 100_000})
|
||||
await mw.aclose()
|
||||
attrs = sink.events[0].attrs
|
||||
print(attrs["ok"], attrs["error"]["_truncated"], attrs["error"]["size"]) # False True 100002
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
**Upgrading to 2026.10.04:** before this release payloads and attrs were
|
||||
stored whole. A row that used to hold more than 64 KiB there now holds the
|
||||
marker; pass `max_payload_bytes=None` to keep storing everything.
|
||||
|
||||
### Redaction
|
||||
|
||||
Secrets are removed from arguments, results, error messages and hook output
|
||||
@@ -311,6 +347,25 @@ instrument(app, result_classifier=soft_error_classifier(error_keys=("failure_rea
|
||||
instrument(app, result_classifier=soft_error_classifier(error_keys=()))
|
||||
```
|
||||
|
||||
`message_keys` sets where the message comes from (`error`, `message`,
|
||||
`detail`, `reason` by default), and both `message_keys` and `error_keys`
|
||||
take dotted paths into nested dicts. A key that literally contains a dot is
|
||||
tried first; a missing step, or one that is not a dict, counts as absent:
|
||||
|
||||
```python
|
||||
from fastmcp_feedback.instrumentation import soft_error_classifier
|
||||
|
||||
# mcardupilot: a wait that returned because the simulator died, cause nested
|
||||
classify = soft_error_classifier(
|
||||
status_keys=("status", "state", "reason"),
|
||||
extra_statuses=("died",),
|
||||
message_keys=("snapshot.error", "error", "message", "detail", "reason"),
|
||||
)
|
||||
died = {"reason": "died", "snapshot": {"error": "SITL exited: segfault"}}
|
||||
assert classify("wait_until", died) == "reason=died: SITL exited: segfault"
|
||||
assert classify("wait_until", {"reason": "timeout", "snapshot": {}}) is None
|
||||
```
|
||||
|
||||
Add your own status values, or pass any `(tool, result) -> str | None`:
|
||||
|
||||
```python
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "fastmcp-feedback"
|
||||
version = "2026.10.02.1" # YYYYMMDD.patch versioning - update for each release
|
||||
version = "2026.10.04" # YYYYMMDD.patch versioning - update for each release
|
||||
description = "Production-ready feedback collection system for FastMCP servers"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
|
||||
@@ -41,7 +41,7 @@ Quick Start:
|
||||
import logging
|
||||
|
||||
# Version information - CalVer YYYY.MM.DD
|
||||
__version__ = "2026.10.02.1"
|
||||
__version__ = "2026.10.04"
|
||||
__author__ = "Ryan Malloy"
|
||||
__email__ = "ryan@supported.systems"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user