use super::BuiltinTool;
use crate::error::NikaError;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::future::Future;
use std::pin::Pin;
pub const COMPLETION_MARKER: &str = "__NIKA_COMPLETE__";
#[derive(Debug, Clone, Deserialize)]
pub struct CompleteParams {
pub result: Value,
#[serde(default)]
pub confidence: Option<f64>,
#[serde(default)]
pub reasoning: Option<String>,
#[serde(default)]
pub metadata: Option<Value>,
}
impl CompleteParams {
pub fn validate(&self) -> Result<(), NikaError> {
if let Some(conf) = self.confidence {
if !(0.0..=1.0).contains(&conf) {
return Err(NikaError::ValidationError {
reason: format!("confidence must be between 0.0 and 1.0, got {}", conf),
});
}
}
Ok(())
}
pub fn result_as_string(&self) -> String {
match &self.result {
Value::String(s) => s.clone(),
other => other.to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompleteResponse {
pub completed: bool,
pub result: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub confidence: Option<f64>,
#[serde(default = "default_marker")]
pub marker: String,
#[serde(default)]
pub is_final: bool,
}
fn default_marker() -> String {
COMPLETION_MARKER.to_string()
}
impl CompleteResponse {
pub fn success(params: &CompleteParams, is_final: bool) -> Self {
Self {
completed: true,
result: params.result.clone(),
confidence: params.confidence,
marker: COMPLETION_MARKER.to_string(),
is_final,
}
}
}
pub struct CompleteTool;
impl BuiltinTool for CompleteTool {
fn name(&self) -> &'static str {
"complete"
}
fn description(&self) -> &'static str {
"Signal task completion with a structured result"
}
fn parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"result": {
"type": "string",
"description": "The final result or answer for the task. Serialize complex values as JSON strings."
},
"confidence": {
"type": "number",
"description": "Confidence level in the result (0.0-1.0)"
},
"reasoning": {
"type": "string",
"description": "Explanation of how you arrived at this result"
}
},
"required": ["result", "confidence", "reasoning"],
"additionalProperties": false
})
}
fn call<'a>(
&'a self,
args: String,
) -> Pin<Box<dyn Future<Output = Result<String, NikaError>> + Send + 'a>> {
Box::pin(async move {
let params: CompleteParams =
serde_json::from_str(&args).map_err(|e| NikaError::BuiltinInvalidParams {
tool: "nika:complete".into(),
reason: format!("Invalid JSON parameters: {}", e),
})?;
params
.validate()
.map_err(|e| NikaError::BuiltinInvalidParams {
tool: "nika:complete".into(),
reason: e.to_string(),
})?;
tracing::debug!(
target: "nika_complete",
confidence = ?params.confidence,
has_reasoning = params.reasoning.is_some(),
"Agent signaling completion"
);
let response = CompleteResponse::success(¶ms, true);
serde_json::to_string(&response).map_err(|e| NikaError::BuiltinToolError {
tool: "nika:complete".into(),
reason: format!("Failed to serialize response: {}", e),
})
})
}
}
pub fn is_completion_signal(tool_name: &str, response: &str) -> bool {
if tool_name != "nika:complete" && tool_name != "complete" {
return false;
}
response.contains(COMPLETION_MARKER)
}
pub fn parse_completion_response(response: &str) -> Option<CompleteResponse> {
serde_json::from_str(response).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_complete_tool_name() {
let tool = CompleteTool;
assert_eq!(tool.name(), "complete");
}
#[test]
fn test_complete_tool_description() {
let tool = CompleteTool;
assert!(tool.description().contains("completion"));
}
#[test]
fn test_complete_tool_schema() {
let tool = CompleteTool;
let schema = tool.parameters_schema();
assert_eq!(schema["type"], "object");
assert!(schema["properties"]["result"].is_object());
assert!(schema["properties"]["confidence"].is_object());
assert!(schema["properties"]["reasoning"].is_object());
assert_eq!(schema["additionalProperties"], false);
assert!(schema["required"]
.as_array()
.unwrap()
.contains(&serde_json::json!("result")));
}
#[tokio::test]
async fn test_complete_simple_string_result() {
let tool = CompleteTool;
let result = tool
.call(r#"{"result": "Task completed successfully"}"#.to_string())
.await;
assert!(result.is_ok());
let response: CompleteResponse = serde_json::from_str(&result.unwrap()).unwrap();
assert!(response.completed);
assert_eq!(response.result, "Task completed successfully");
assert_eq!(response.marker, COMPLETION_MARKER);
assert!(response.is_final);
}
#[tokio::test]
async fn test_complete_with_confidence() {
let tool = CompleteTool;
let result = tool
.call(r#"{"result": "Answer", "confidence": 0.95}"#.to_string())
.await;
assert!(result.is_ok());
let response: CompleteResponse = serde_json::from_str(&result.unwrap()).unwrap();
assert!(response.completed);
assert_eq!(response.confidence, Some(0.95));
}
#[tokio::test]
async fn test_complete_with_reasoning() {
let tool = CompleteTool;
let result = tool
.call(r#"{"result": "42", "reasoning": "Based on the calculation..."}"#.to_string())
.await;
assert!(result.is_ok());
let response: CompleteResponse = serde_json::from_str(&result.unwrap()).unwrap();
assert!(response.completed);
}
#[tokio::test]
async fn test_complete_with_complex_result() {
let tool = CompleteTool;
let result = tool
.call(
r#"{
"result": {"items": [1, 2, 3], "total": 6},
"confidence": 0.99
}"#
.to_string(),
)
.await;
assert!(result.is_ok());
let response: CompleteResponse = serde_json::from_str(&result.unwrap()).unwrap();
assert!(response.completed);
assert_eq!(response.result["items"][0], 1);
assert_eq!(response.result["total"], 6);
}
#[tokio::test]
async fn test_complete_invalid_confidence_too_high() {
let tool = CompleteTool;
let result = tool
.call(r#"{"result": "x", "confidence": 1.5}"#.to_string())
.await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("confidence"));
}
#[tokio::test]
async fn test_complete_invalid_confidence_negative() {
let tool = CompleteTool;
let result = tool
.call(r#"{"result": "x", "confidence": -0.1}"#.to_string())
.await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("confidence"));
}
#[tokio::test]
async fn test_complete_missing_result() {
let tool = CompleteTool;
let result = tool.call(r#"{"confidence": 0.9}"#.to_string()).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("Invalid JSON parameters"));
}
#[tokio::test]
async fn test_complete_invalid_json() {
let tool = CompleteTool;
let result = tool.call("not json".to_string()).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("Invalid JSON parameters"));
}
#[test]
fn test_all_properties_have_type_field() {
let tool = CompleteTool;
let schema = tool.parameters_schema();
let props = schema["properties"]
.as_object()
.expect("properties must be an object");
for (name, prop_schema) in props {
assert!(
prop_schema.get("type").is_some(),
"Property '{}' missing 'type' field — OpenAI will reject this schema",
name,
);
}
}
#[test]
fn test_is_completion_signal_positive() {
let response = serde_json::to_string(&CompleteResponse {
completed: true,
result: Value::String("done".into()),
confidence: Some(0.9),
marker: COMPLETION_MARKER.to_string(),
is_final: true,
})
.unwrap();
assert!(is_completion_signal("nika:complete", &response));
assert!(is_completion_signal("complete", &response));
}
#[test]
fn test_is_completion_signal_negative_wrong_tool() {
let response = format!(r#"{{"marker": "{}"}}"#, COMPLETION_MARKER);
assert!(!is_completion_signal("nika:emit", &response));
}
#[test]
fn test_is_completion_signal_negative_no_marker() {
let response = r#"{"completed": true}"#;
assert!(!is_completion_signal("nika:complete", response));
}
#[test]
fn test_parse_completion_response() {
let response = serde_json::to_string(&CompleteResponse {
completed: true,
result: Value::String("test".into()),
confidence: Some(0.8),
marker: COMPLETION_MARKER.to_string(),
is_final: true,
})
.unwrap();
let parsed = parse_completion_response(&response).unwrap();
assert!(parsed.completed);
assert_eq!(parsed.result, "test");
assert_eq!(parsed.confidence, Some(0.8));
}
#[test]
fn test_complete_params_validate_valid() {
let params = CompleteParams {
result: Value::String("ok".into()),
confidence: Some(0.5),
reasoning: None,
metadata: None,
};
assert!(params.validate().is_ok());
}
#[test]
fn test_complete_params_result_as_string() {
let params = CompleteParams {
result: Value::String("hello".into()),
confidence: None,
reasoning: None,
metadata: None,
};
assert_eq!(params.result_as_string(), "hello");
let params = CompleteParams {
result: serde_json::json!(42),
confidence: None,
reasoning: None,
metadata: None,
};
assert_eq!(params.result_as_string(), "42");
let params = CompleteParams {
result: serde_json::json!({"key": "value"}),
confidence: None,
reasoning: None,
metadata: None,
};
assert!(params.result_as_string().contains("key"));
}
#[test]
fn test_complete_response_success() {
let params = CompleteParams {
result: Value::String("done".into()),
confidence: Some(0.99),
reasoning: Some("explanation".into()),
metadata: None,
};
let response = CompleteResponse::success(¶ms, true);
assert!(response.completed);
assert_eq!(response.result, "done");
assert_eq!(response.confidence, Some(0.99));
assert!(response.is_final);
assert_eq!(response.marker, COMPLETION_MARKER);
}
}