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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

---

## [bridge-v0.3.3] — 2026-06-04

### Added
- `record_llm_call` accepts the optional prompt-cache breakdown (`cache_read_tokens` / `cache_creation_tokens` / `uncached_input_tokens`) and folds it onto the event `args` — but only when caching is active. A prompt-cache hit is now provable from a bridge-written journal too, at parity with korgex's local-journal and HTTP transports. A cold turn keeps the legacy two-field `args`, so older readers and the hash-chain over historical events are undisturbed (field order is irrelevant — `args` canonicalize with sorted keys at hash time).

---

## [bridge-v0.3.2] — 2026-05-27

### Added
Expand Down Expand Up @@ -117,3 +124,4 @@ See [ROADMAP.md](ROADMAP.md) for planned features.
[bridge-v0.3.0]: https://github.com/New1Direction/korg/releases/tag/bridge-v0.3.0
[bridge-v0.3.1]: https://github.com/New1Direction/korg/releases/tag/bridge-v0.3.1
[bridge-v0.3.2]: https://github.com/New1Direction/korg/releases/tag/bridge-v0.3.2
[bridge-v0.3.3]: https://github.com/New1Direction/korg/releases/tag/bridge-v0.3.3
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/korg-bridge/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "korg-bridge"
version = "0.3.2"
version = "0.3.3"
edition = "2021"
description = "PyO3 bridge — exposes Korg's in-process WAL (CapabilityJournal) to Python so korgex / KorgChat can write directly without HTTP."
license = "MIT OR Apache-2.0"
Expand Down
6 changes: 6 additions & 0 deletions crates/korg-bridge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ byte-for-byte under both `korg_registry::ledger_chain::verify_chain` (Rust) and
korgex's `src/ledger_spec.verify_chain` (Python): the two ledger paths
(korgchat-via-bridge, korgex) collapse onto one chained substrate.

`record_llm_call` also carries an optional prompt-cache breakdown
(`cache_read_tokens` / `cache_creation_tokens` / `uncached_input_tokens`), folded
onto the event `args` only when caching is active — so a cache hit is provable
from the journal too, at parity with korgex's local-journal and HTTP transports.
A cold turn keeps the legacy two-field `args`.

Build the wheel and verify:

```bash
Expand Down
2 changes: 1 addition & 1 deletion crates/korg-bridge/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "maturin"

[project]
name = "korg_bridge"
version = "0.3.2"
version = "0.3.3"
description = "In-process Python ↔ Korg WAL bridge (PyO3). Replaces the HTTP loop that korg_ledger.py used to use."
requires-python = ">=3.8"
readme = "README.md"
Expand Down
44 changes: 40 additions & 4 deletions crates/korg-bridge/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ impl Bridge {
/// consumers (search, audit, replay) can find it. Token counts stay in
/// the same places as before. Callers responsible for content-addressing
/// large replies — the bridge writes whatever text it's given.
///
/// v0.3.3: optional prompt-cache breakdown (`cache_read_tokens` /
/// `cache_creation_tokens` / `uncached_input_tokens`). Folded onto `args`
/// only when caching is active, so a cache hit is provable from the
/// bridge-written journal too. A cold turn keeps the legacy `args` shape.
#[pyo3(signature = (
model,
prompt_tokens,
Expand All @@ -144,6 +149,9 @@ impl Bridge {
triggered_by,
source_agent = "agent:korgex@0.3.0",
assistant_text = None,
cache_read_tokens = 0,
cache_creation_tokens = 0,
uncached_input_tokens = None,
))]
fn record_llm_call(
&self,
Expand All @@ -154,11 +162,39 @@ impl Bridge {
triggered_by: Option<u64>,
source_agent: &str,
assistant_text: Option<&str>,
cache_read_tokens: u64,
cache_creation_tokens: u64,
uncached_input_tokens: Option<u64>,
) -> PyResult<u64> {
let args = serde_json::json!({
"model": model,
"prompt_tokens": prompt_tokens,
});
// Prompt-cache breakdown — mirrors src/korg_ledger.py::_llm_call_args so the
// bridge transport carries the same provable cache data as the local-journal
// and HTTP paths. Folded in ONLY when caching is active (a read or a write
// happened); a cold turn keeps the legacy two-field shape, so older readers
// and the hash-chain over historical events are undisturbed. Field order is
// irrelevant — args are canonicalised with sorted keys at hash time.
let mut args_map = serde_json::Map::new();
args_map.insert("model".to_string(), serde_json::Value::from(model));
args_map.insert(
"prompt_tokens".to_string(),
serde_json::Value::from(prompt_tokens),
);
if cache_read_tokens != 0 || cache_creation_tokens != 0 {
args_map.insert(
"cache_read_tokens".to_string(),
serde_json::Value::from(cache_read_tokens),
);
args_map.insert(
"cache_creation_tokens".to_string(),
serde_json::Value::from(cache_creation_tokens),
);
if let Some(uncached) = uncached_input_tokens {
args_map.insert(
"uncached_input_tokens".to_string(),
serde_json::Value::from(uncached),
);
}
}
let args = serde_json::Value::Object(args_map);
// result carries completion_tokens + (since v0.3.2) optional text.
// Keep the field name "text" — short, obvious, and matches what a
// future content-addressed variant would also use as a key.
Expand Down
56 changes: 55 additions & 1 deletion crates/korg-bridge/tests/test_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def _read_events(journal_path: Path) -> list[dict]:


def test_module_version_present():
assert korg_bridge.__version__ == "0.3.2"
assert korg_bridge.__version__ == "0.3.3"


def test_record_llm_call_assistant_text_optional(tmp_journal):
Expand Down Expand Up @@ -59,6 +59,60 @@ def test_record_llm_call_assistant_text_optional(tmp_journal):
}


def test_record_llm_call_cache_breakdown(tmp_journal):
"""v0.3.3: the prompt-cache breakdown lands on the event args when caching is
active, so a cache hit is provable from the bridge-written journal too — parity
with the local-journal and HTTP transports."""
bridge = korg_bridge.Bridge(str(tmp_journal))
# 1. cold turn (no cache) → legacy two-field args, byte-identical to before
bridge.record_llm_call(
model="claude-sonnet-4-6",
prompt_tokens=500,
completion_tokens=5,
duration_ms=100,
triggered_by=None,
)
# 2. warm turn → disjoint breakdown folded in
bridge.record_llm_call(
model="claude-sonnet-4-6",
prompt_tokens=400,
completion_tokens=8,
duration_ms=120,
triggered_by=1,
cache_read_tokens=800,
cache_creation_tokens=50,
uncached_input_tokens=400,
)
events = _read_events(tmp_journal)
assert events[0]["event"]["args"] == {"model": "claude-sonnet-4-6", "prompt_tokens": 500}
assert events[1]["event"]["args"] == {
"model": "claude-sonnet-4-6",
"prompt_tokens": 400,
"cache_read_tokens": 800,
"cache_creation_tokens": 50,
"uncached_input_tokens": 400,
}


def test_record_llm_call_cache_creation_only(tmp_journal):
"""A cold turn that WRITES the cache (creation>0, read==0) is still a cache
event — the breakdown is recorded."""
bridge = korg_bridge.Bridge(str(tmp_journal))
bridge.record_llm_call(
model="claude-sonnet-4-6",
prompt_tokens=1000,
completion_tokens=8,
duration_ms=120,
triggered_by=None,
cache_read_tokens=0,
cache_creation_tokens=1000,
uncached_input_tokens=1000,
)
events = _read_events(tmp_journal)
assert events[0]["event"]["args"]["cache_creation_tokens"] == 1000
assert events[0]["event"]["args"]["uncached_input_tokens"] == 1000


def test_repr_initial_state(tmp_journal):
bridge = korg_bridge.Bridge(str(tmp_journal))
rep = repr(bridge)
Expand Down
Loading