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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Fixed

- fix(memory): use `wait=true` on Qdrant upsert to eliminate testcontainer timing race — points are now indexed and queryable immediately after `upsert` returns (closes #2413)

### Added (tests)

- test(core): add `multi_layer_before_after_tool_ordering` — verifies both layers are called in FIFO order for `before_tool` and `after_tool` (#2361)

- fix(classifiers): sha2 0.11 hex formatting — replace `format!("{:x}", ...)` with `hex::encode(...)` in `verify_sha256` and its test helper (#2401)
- fix(deps): bump sha2 0.10→0.11, ordered-float 5.1→5.3, proptest 1.10→1.11, toml 1.0→1.1, uuid 1.22→1.23 (#2401)
- fix(skills): `two_stage_matching` and `confusability_threshold` config fields are now applied at agent startup; `AgentBuilder` gains `with_two_stage_matching` and `with_confusability_threshold` builder methods wired in `runner.rs` and `daemon.rs` (closes #2404)
Expand Down
8 changes: 4 additions & 4 deletions crates/zeph-core/src/agent/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1669,14 +1669,14 @@ mod tests {
);

let agent = make_agent().with_confusability_threshold(1.5);
assert_eq!(
agent.skill_state.confusability_threshold, 1.0,
assert!(
(agent.skill_state.confusability_threshold - 1.0).abs() < f32::EPSILON,
"with_confusability_threshold must clamp values above 1.0"
);

let agent = make_agent().with_confusability_threshold(-0.1);
assert_eq!(
agent.skill_state.confusability_threshold, 0.0,
assert!(
agent.skill_state.confusability_threshold.abs() < f32::EPSILON,
"with_confusability_threshold must clamp values below 0.0"
);
}
Expand Down
75 changes: 75 additions & 0 deletions crates/zeph-core/src/runtime_layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,81 @@ mod tests {
);
}

/// Two layers registered in order [A, B]: `before_tool` must fire A then B,
/// and `after_tool` must fire A then B (forward order for both).
#[tokio::test]
async fn multi_layer_before_after_tool_ordering() {
use std::sync::{Arc, Mutex};

struct ToolOrderLayer {
id: u32,
log: Arc<Mutex<Vec<String>>>,
}
impl RuntimeLayer for ToolOrderLayer {
fn before_tool<'a>(
&'a self,
_ctx: &'a LayerContext<'_>,
_call: &'a ToolCall,
) -> Pin<Box<dyn Future<Output = BeforeToolResult> + Send + 'a>> {
self.log
.lock()
.unwrap()
.push(format!("before_tool_{}", self.id));
Box::pin(std::future::ready(None))
}

fn after_tool<'a>(
&'a self,
_ctx: &'a LayerContext<'_>,
_call: &'a ToolCall,
_result: &'a Result<Option<ToolOutput>, ToolError>,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
self.log
.lock()
.unwrap()
.push(format!("after_tool_{}", self.id));
Box::pin(std::future::ready(()))
}
}

let log: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let layer_a = ToolOrderLayer {
id: 1,
log: Arc::clone(&log),
};
let layer_b = ToolOrderLayer {
id: 2,
log: Arc::clone(&log),
};

let ctx = LayerContext {
conversation_id: None,
turn_number: 0,
};
let call = ToolCall {
tool_id: "shell".into(),
params: serde_json::Map::new(),
};
let result: Result<Option<ToolOutput>, ToolError> = Ok(None);

layer_a.before_tool(&ctx, &call).await;
layer_b.before_tool(&ctx, &call).await;
layer_a.after_tool(&ctx, &call, &result).await;
layer_b.after_tool(&ctx, &call, &result).await;

let events = log.lock().unwrap().clone();
assert_eq!(
events,
vec![
"before_tool_1",
"before_tool_2",
"after_tool_1",
"after_tool_2"
],
"tool hooks must fire in registration order"
);
}

/// `NoopLayer` `after_tool` returns `()` without errors.
#[tokio::test]
async fn noop_layer_after_tool_returns_unit() {
Expand Down
4 changes: 1 addition & 3 deletions crates/zeph-memory/src/qdrant_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,8 @@ impl QdrantOps {
///
/// Returns an error if the upsert fails.
pub async fn upsert(&self, collection: &str, points: Vec<PointStruct>) -> QdrantResult<()> {
// wait(false): fire-and-forget on the hot path — saves 3-15ms per embedding store call.
// Qdrant guarantees eventual consistency; read-your-writes is not required here.
self.client
.upsert_points(UpsertPointsBuilder::new(collection, points).wait(false))
.upsert_points(UpsertPointsBuilder::new(collection, points).wait(true))
.await
.map_err(Box::new)?;
Ok(())
Expand Down
Loading