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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [0.8.1] — 2026-10-09

### Fixed
- The usage report no longer renders an absent per-step `messages` count as a measured `0`; it is `null` in the status JSON when unknown (found by the pre-tag scan).
- `read_file` rejects an empty `path` with a clear message instead of a confusing "is a directory" error.

### Added
- `tole status` and the serve status JSON now report token usage, cache-hit rate and a per-step request-size split (system+tools vs history, stored per step under `tole_wire` in the usage record); `fact/mission` gains `cached_tokens`. Instrumentation only: the wire, budgets and durable entry shapes are unchanged (#211, part 1 of 3).

Expand Down
16 changes: 14 additions & 2 deletions crates/tole-cli/src/usage_report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ pub struct WirePoint {
pub system_chars: u64,
pub tools_chars: u64,
pub history_chars: u64,
pub messages: u64,
/// Message count of the request; `None` when the record does not carry it
/// (unknown is never rendered as a measured zero).
pub messages: Option<u64>,
}

#[derive(Debug, Clone, PartialEq)]
Expand Down Expand Up @@ -101,7 +103,7 @@ fn wire_of(u: &Value) -> Option<WirePoint> {
system_chars,
tools_chars,
history_chars,
messages: w.get("messages").and_then(Value::as_u64).unwrap_or(0),
messages: w.get("messages").and_then(Value::as_u64),
})
}

Expand Down Expand Up @@ -417,4 +419,14 @@ mod tests {
assert_eq!(r.prompt_tokens, 0);
assert!(r.wire_first.is_none());
}

#[test]
fn missing_messages_is_unknown_not_zero() {
let r = usage_report(&[rec(
json!({"tole_wire": {"system_chars": 1, "tools_chars": 2, "history_chars": 3}}),
)]);
let w = r.wire_first.expect("wire point without messages is kept");
assert_eq!(w.messages, None);
assert_eq!(wire_json(&w)["messages"], Value::Null);
}
}
10 changes: 10 additions & 0 deletions crates/tole-core/src/read_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ impl ReadFileTool {
/// also an escape. Errors distinguish escape from plain not-found so
/// the model gets actionable feedback.
fn jailed(&self, rel: &str) -> Result<PathBuf, String> {
if rel.is_empty() {
return Err("read_file: path is empty".to_string());
}
let rel_path = Path::new(rel);
// Component-based validation (uniform with the other file tools;
// CodeCora scan #6/#12): drive-prefixed/rooted Windows relatives
Expand Down Expand Up @@ -207,6 +210,13 @@ mod tests {
assert_eq!(out["bytes"], json!(5));
}

#[test]
fn rejects_empty_path() {
let t = ReadFileTool::new(tmpdir("empty"));
let err = t.execute(json!({ "path": "" })).unwrap_err();
assert!(err.to_string().contains("path is empty"), "{err}");
}

#[test]
fn rejects_absolute_and_traversal() {
let dir = tmpdir("jail");
Expand Down
Loading