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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes to the `fipsagents` package will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/).

## [0.14.2] - 2026-04-28

### Added

- **`HttpSessionStore.get_cost_data` reads from the platform.** Replaces the `NotImplementedError` placeholder with a real `GET /v1/sessions/{id}/cost_data`, closing the cumulative-cost gap noted in 0.14.0's release notes. HTTP-backed deployments now get the same cumulative shallow-merge semantics that SQLite/Postgres provide natively — the per-turn accumulator on `OpenAIChatServer` reads existing totals before computing the merge, so multi-turn sessions converge on cumulative numbers instead of last-write-wins. 5 new tests across the unit and e2e suites; total `fipsagents` suite 765 → 770.

### Notes

- Requires `fipsagents-platform>=0.2.1`. Older platforms 404 cleanly on the read path and the agent degrades to the previous last-write-wins behavior — same operational shape as 0.14.0/0.14.1, no breakage.

## [0.14.1] - 2026-04-27

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion packages/fipsagents/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "fipsagents"
version = "0.14.1"
version = "0.14.2"
description = "Production-ready AI agent framework for FIPS/OpenShift environments"
readme = "README.md"
license = {file = "LICENSE"}
Expand Down
22 changes: 12 additions & 10 deletions packages/fipsagents/src/fipsagents/server/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,17 +252,19 @@ async def update(
return status != 404

async def get_cost_data(self, session_id: str) -> dict:
# The platform service has no GET /v1/sessions/{id}/cost_data
# endpoint yet. Until it does, callers must treat the HTTP
# backend as write-only for cost accumulator state. The server's
# per-turn accumulator catches NotImplementedError and treats
# the existing total as empty (so the next write is the turn's
# delta rather than a true cumulative). A follow-up issue tracks
# exposing the read endpoint on the platform.
raise NotImplementedError(
"HttpSessionStore.get_cost_data: the platform service does "
"not expose a GET endpoint for cost_data yet."
# Mirrors the SQLite/Postgres contract: empty dict when the
# session is missing or has no cost_data yet. Requires
# fipsagents-platform >= 0.2.1 (which exposes the GET endpoint);
# against older platforms the route 404s on every call and we
# degrade gracefully to last-write-wins semantics.
status, data = await self._client.request(
"GET",
f"/v1/sessions/{session_id}/cost_data",
not_found_returns_none=True,
)
if status == 404 or data is None:
return {}
return data.get("cost_data") or {}

async def delete(self, session_id: str) -> bool:
status, _ = await self._client.request(
Expand Down
40 changes: 32 additions & 8 deletions packages/fipsagents/tests/test_http_stores.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,17 +221,41 @@ async def test_session_update_none_cost_data_delegates_to_exists() -> None:


@pytest.mark.asyncio
async def test_session_get_cost_data_raises_not_implemented() -> None:
"""The HTTP backend has no GET cost_data endpoint yet -- raise so the
server-side accumulator can fall back to a delta-only write."""
rec = _Recorder([])
async def test_session_get_cost_data_returns_dict() -> None:
"""GET /v1/sessions/{id}/cost_data parses the cumulative dict."""
rec = _Recorder([_ok(200, {
"session_id": "sess_abc",
"cost_data": {"input_tokens": 100, "output_tokens": 50},
})])
store = HttpSessionStore(
"http://platform.test", transport=httpx.MockTransport(rec),
)
result = await store.get_cost_data("sess_abc")
assert result == {"input_tokens": 100, "output_tokens": 50}
assert rec.requests[0].method == "GET"
assert rec.requests[0].url.path == "/v1/sessions/sess_abc/cost_data"
await store.close()


@pytest.mark.asyncio
async def test_session_get_cost_data_404_returns_empty() -> None:
"""Missing sessions degrade to {} per the ABC contract."""
rec = _Recorder([_ok(404, {"detail": "not found"})])
store = HttpSessionStore(
"http://platform.test", transport=httpx.MockTransport(rec),
)
assert await store.get_cost_data("missing") == {}
await store.close()


@pytest.mark.asyncio
async def test_session_get_cost_data_empty_dict_round_trips() -> None:
"""Existing session with no writes returns the platform's empty dict."""
rec = _Recorder([_ok(200, {"session_id": "sess_empty", "cost_data": {}})])
store = HttpSessionStore(
"http://platform.test", transport=httpx.MockTransport(rec),
)
with pytest.raises(NotImplementedError):
await store.get_cost_data("sess_anything")
# No HTTP request should have been issued.
assert rec.requests == []
assert await store.get_cost_data("sess_empty") == {}
await store.close()


Expand Down
25 changes: 25 additions & 0 deletions packages/fipsagents/tests/test_http_stores_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,31 @@ async def test_session_update_none_cost_data_returns_existence(
await store.close()


@pytest.mark.asyncio
async def test_session_get_cost_data_round_trip(platform_transport) -> None:
"""Two PATCHes then GET — the returned dict is the cumulative merge."""
store = HttpSessionStore(
"http://platform.test", transport=platform_transport,
)
sid = await store.create("sess_e2e_get_cost")
await store.update(sid, cost_data={"input_tokens": 10, "output_tokens": 5})
await store.update(sid, cost_data={"input_tokens": 30, "requests": 2})

result = await store.get_cost_data(sid)
assert result == {"input_tokens": 30, "output_tokens": 5, "requests": 2}
await store.close()


@pytest.mark.asyncio
async def test_session_get_cost_data_missing_returns_empty(platform_transport) -> None:
"""Platform 404 → ABC-compliant empty dict on the agent side."""
store = HttpSessionStore(
"http://platform.test", transport=platform_transport,
)
assert await store.get_cost_data("sess_e2e_never_existed") == {}
await store.close()


# ---------------------------------------------------------------------------
# Traces
# ---------------------------------------------------------------------------
Expand Down