nika_engine/runtime/builtin/
complete.rs1use super::BuiltinTool;
34use crate::error::NikaError;
35use serde::{Deserialize, Serialize};
36use serde_json::Value;
37use std::future::Future;
38use std::pin::Pin;
39
40pub const COMPLETION_MARKER: &str = "__NIKA_COMPLETE__";
46
47#[derive(Debug, Clone, Deserialize)]
53pub struct CompleteParams {
54 pub result: Value,
56
57 #[serde(default)]
59 pub confidence: Option<f64>,
60
61 #[serde(default)]
63 pub reasoning: Option<String>,
64
65 #[serde(default)]
67 pub metadata: Option<Value>,
68}
69
70impl CompleteParams {
71 pub fn validate(&self) -> Result<(), NikaError> {
73 if let Some(conf) = self.confidence {
75 if !(0.0..=1.0).contains(&conf) {
76 return Err(NikaError::ValidationError {
77 reason: format!("confidence must be between 0.0 and 1.0, got {}", conf),
78 });
79 }
80 }
81
82 Ok(())
83 }
84
85 pub fn result_as_string(&self) -> String {
87 match &self.result {
88 Value::String(s) => s.clone(),
89 other => other.to_string(),
90 }
91 }
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct CompleteResponse {
101 pub completed: bool,
103
104 pub result: Value,
106
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub confidence: Option<f64>,
110
111 #[serde(default = "default_marker")]
113 pub marker: String,
114
115 #[serde(default)]
117 pub is_final: bool,
118}
119
120fn default_marker() -> String {
121 COMPLETION_MARKER.to_string()
122}
123
124impl CompleteResponse {
125 pub fn success(params: &CompleteParams, is_final: bool) -> Self {
127 Self {
128 completed: true,
129 result: params.result.clone(),
130 confidence: params.confidence,
131 marker: COMPLETION_MARKER.to_string(),
132 is_final,
133 }
134 }
135}
136
137pub struct CompleteTool;
146
147impl BuiltinTool for CompleteTool {
148 fn name(&self) -> &'static str {
149 "complete"
150 }
151
152 fn description(&self) -> &'static str {
153 "Signal task completion with a structured result"
154 }
155
156 fn parameters_schema(&self) -> serde_json::Value {
157 serde_json::json!({
158 "type": "object",
159 "properties": {
160 "result": {
161 "type": "string",
162 "description": "The final result or answer for the task. Serialize complex values as JSON strings."
163 },
164 "confidence": {
165 "type": "number",
166 "description": "Confidence level in the result (0.0-1.0)"
167 },
168 "reasoning": {
169 "type": "string",
170 "description": "Explanation of how you arrived at this result"
171 }
172 },
173 "required": ["result", "confidence", "reasoning"],
174 "additionalProperties": false
175 })
176 }
177
178 fn call<'a>(
179 &'a self,
180 args: String,
181 ) -> Pin<Box<dyn Future<Output = Result<String, NikaError>> + Send + 'a>> {
182 Box::pin(async move {
183 let params: CompleteParams =
185 serde_json::from_str(&args).map_err(|e| NikaError::BuiltinInvalidParams {
186 tool: "nika:complete".into(),
187 reason: format!("Invalid JSON parameters: {}", e),
188 })?;
189
190 params
192 .validate()
193 .map_err(|e| NikaError::BuiltinInvalidParams {
194 tool: "nika:complete".into(),
195 reason: e.to_string(),
196 })?;
197
198 tracing::debug!(
199 target: "nika_complete",
200 confidence = ?params.confidence,
201 has_reasoning = params.reasoning.is_some(),
202 "Agent signaling completion"
203 );
204
205 let response = CompleteResponse::success(¶ms, true);
208
209 serde_json::to_string(&response).map_err(|e| NikaError::BuiltinToolError {
210 tool: "nika:complete".into(),
211 reason: format!("Failed to serialize response: {}", e),
212 })
213 })
214 }
215}
216
217pub fn is_completion_signal(tool_name: &str, response: &str) -> bool {
223 if tool_name != "nika:complete" && tool_name != "complete" {
224 return false;
225 }
226
227 response.contains(COMPLETION_MARKER)
229}
230
231pub fn parse_completion_response(response: &str) -> Option<CompleteResponse> {
233 serde_json::from_str(response).ok()
234}
235
236#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn test_complete_tool_name() {
246 let tool = CompleteTool;
247 assert_eq!(tool.name(), "complete");
248 }
249
250 #[test]
251 fn test_complete_tool_description() {
252 let tool = CompleteTool;
253 assert!(tool.description().contains("completion"));
254 }
255
256 #[test]
257 fn test_complete_tool_schema() {
258 let tool = CompleteTool;
259 let schema = tool.parameters_schema();
260 assert_eq!(schema["type"], "object");
261 assert!(schema["properties"]["result"].is_object());
262 assert!(schema["properties"]["confidence"].is_object());
263 assert!(schema["properties"]["reasoning"].is_object());
264 assert_eq!(schema["additionalProperties"], false);
265 assert!(schema["required"]
266 .as_array()
267 .unwrap()
268 .contains(&serde_json::json!("result")));
269 }
270
271 #[tokio::test]
272 async fn test_complete_simple_string_result() {
273 let tool = CompleteTool;
274 let result = tool
275 .call(r#"{"result": "Task completed successfully"}"#.to_string())
276 .await;
277
278 assert!(result.is_ok());
279 let response: CompleteResponse = serde_json::from_str(&result.unwrap()).unwrap();
280 assert!(response.completed);
281 assert_eq!(response.result, "Task completed successfully");
282 assert_eq!(response.marker, COMPLETION_MARKER);
283 assert!(response.is_final);
284 }
285
286 #[tokio::test]
287 async fn test_complete_with_confidence() {
288 let tool = CompleteTool;
289 let result = tool
290 .call(r#"{"result": "Answer", "confidence": 0.95}"#.to_string())
291 .await;
292
293 assert!(result.is_ok());
294 let response: CompleteResponse = serde_json::from_str(&result.unwrap()).unwrap();
295 assert!(response.completed);
296 assert_eq!(response.confidence, Some(0.95));
297 }
298
299 #[tokio::test]
300 async fn test_complete_with_reasoning() {
301 let tool = CompleteTool;
302 let result = tool
303 .call(r#"{"result": "42", "reasoning": "Based on the calculation..."}"#.to_string())
304 .await;
305
306 assert!(result.is_ok());
307 let response: CompleteResponse = serde_json::from_str(&result.unwrap()).unwrap();
308 assert!(response.completed);
309 }
310
311 #[tokio::test]
312 async fn test_complete_with_complex_result() {
313 let tool = CompleteTool;
314 let result = tool
315 .call(
316 r#"{
317 "result": {"items": [1, 2, 3], "total": 6},
318 "confidence": 0.99
319 }"#
320 .to_string(),
321 )
322 .await;
323
324 assert!(result.is_ok());
325 let response: CompleteResponse = serde_json::from_str(&result.unwrap()).unwrap();
326 assert!(response.completed);
327 assert_eq!(response.result["items"][0], 1);
328 assert_eq!(response.result["total"], 6);
329 }
330
331 #[tokio::test]
332 async fn test_complete_invalid_confidence_too_high() {
333 let tool = CompleteTool;
334 let result = tool
335 .call(r#"{"result": "x", "confidence": 1.5}"#.to_string())
336 .await;
337
338 assert!(result.is_err());
339 let err = result.unwrap_err();
340 assert!(err.to_string().contains("confidence"));
341 }
342
343 #[tokio::test]
344 async fn test_complete_invalid_confidence_negative() {
345 let tool = CompleteTool;
346 let result = tool
347 .call(r#"{"result": "x", "confidence": -0.1}"#.to_string())
348 .await;
349
350 assert!(result.is_err());
351 let err = result.unwrap_err();
352 assert!(err.to_string().contains("confidence"));
353 }
354
355 #[tokio::test]
356 async fn test_complete_missing_result() {
357 let tool = CompleteTool;
358 let result = tool.call(r#"{"confidence": 0.9}"#.to_string()).await;
359
360 assert!(result.is_err());
361 let err = result.unwrap_err();
362 assert!(err.to_string().contains("Invalid JSON parameters"));
363 }
364
365 #[tokio::test]
366 async fn test_complete_invalid_json() {
367 let tool = CompleteTool;
368 let result = tool.call("not json".to_string()).await;
369
370 assert!(result.is_err());
371 let err = result.unwrap_err();
372 assert!(err.to_string().contains("Invalid JSON parameters"));
373 }
374
375 #[test]
378 fn test_all_properties_have_type_field() {
379 let tool = CompleteTool;
380 let schema = tool.parameters_schema();
381 let props = schema["properties"]
382 .as_object()
383 .expect("properties must be an object");
384 for (name, prop_schema) in props {
385 assert!(
386 prop_schema.get("type").is_some(),
387 "Property '{}' missing 'type' field — OpenAI will reject this schema",
388 name,
389 );
390 }
391 }
392
393 #[test]
394 fn test_is_completion_signal_positive() {
395 let response = serde_json::to_string(&CompleteResponse {
396 completed: true,
397 result: Value::String("done".into()),
398 confidence: Some(0.9),
399 marker: COMPLETION_MARKER.to_string(),
400 is_final: true,
401 })
402 .unwrap();
403
404 assert!(is_completion_signal("nika:complete", &response));
405 assert!(is_completion_signal("complete", &response));
406 }
407
408 #[test]
409 fn test_is_completion_signal_negative_wrong_tool() {
410 let response = format!(r#"{{"marker": "{}"}}"#, COMPLETION_MARKER);
411 assert!(!is_completion_signal("nika:emit", &response));
412 }
413
414 #[test]
415 fn test_is_completion_signal_negative_no_marker() {
416 let response = r#"{"completed": true}"#;
417 assert!(!is_completion_signal("nika:complete", response));
418 }
419
420 #[test]
421 fn test_parse_completion_response() {
422 let response = serde_json::to_string(&CompleteResponse {
423 completed: true,
424 result: Value::String("test".into()),
425 confidence: Some(0.8),
426 marker: COMPLETION_MARKER.to_string(),
427 is_final: true,
428 })
429 .unwrap();
430
431 let parsed = parse_completion_response(&response).unwrap();
432 assert!(parsed.completed);
433 assert_eq!(parsed.result, "test");
434 assert_eq!(parsed.confidence, Some(0.8));
435 }
436
437 #[test]
438 fn test_complete_params_validate_valid() {
439 let params = CompleteParams {
440 result: Value::String("ok".into()),
441 confidence: Some(0.5),
442 reasoning: None,
443 metadata: None,
444 };
445 assert!(params.validate().is_ok());
446 }
447
448 #[test]
449 fn test_complete_params_result_as_string() {
450 let params = CompleteParams {
452 result: Value::String("hello".into()),
453 confidence: None,
454 reasoning: None,
455 metadata: None,
456 };
457 assert_eq!(params.result_as_string(), "hello");
458
459 let params = CompleteParams {
461 result: serde_json::json!(42),
462 confidence: None,
463 reasoning: None,
464 metadata: None,
465 };
466 assert_eq!(params.result_as_string(), "42");
467
468 let params = CompleteParams {
470 result: serde_json::json!({"key": "value"}),
471 confidence: None,
472 reasoning: None,
473 metadata: None,
474 };
475 assert!(params.result_as_string().contains("key"));
476 }
477
478 #[test]
479 fn test_complete_response_success() {
480 let params = CompleteParams {
481 result: Value::String("done".into()),
482 confidence: Some(0.99),
483 reasoning: Some("explanation".into()),
484 metadata: None,
485 };
486
487 let response = CompleteResponse::success(¶ms, true);
488 assert!(response.completed);
489 assert_eq!(response.result, "done");
490 assert_eq!(response.confidence, Some(0.99));
491 assert!(response.is_final);
492 assert_eq!(response.marker, COMPLETION_MARKER);
493 }
494}