diff --git a/crates/aprender-serve/src/api/cuda_chat_backend.rs b/crates/aprender-serve/src/api/cuda_chat_backend.rs index 76f9d7e9f8..79831007e9 100644 --- a/crates/aprender-serve/src/api/cuda_chat_backend.rs +++ b/crates/aprender-serve/src/api/cuda_chat_backend.rs @@ -27,11 +27,12 @@ fn try_safetensors_cuda_backend( // #4007: the loaded model's architecture, not the client's `model` string. // #3723: the request's thinking mode; an ON the template cannot express is refused by name. - let prompt = match crate::api::realize_handlers::format_chat_messages_for_state_thinking( + let prompt = match crate::api::realize_handlers::format_chat_messages_for_state_thinking_tools( state, &request.messages, Some(&request.model), request.thinking(), + request.tools.as_deref(), ) { Ok(p) => p, Err(e) => return Some(fail_response(state, StatusCode::BAD_REQUEST, e.to_string())), @@ -121,7 +122,7 @@ async fn try_cuda_backend( // D5: refused before any path (streaming, batch scheduler, direct) takes the // model, so the client gets a 400 rather than a 500 or a mid-stream error. let tokenized = - tokenize_chat_prompt(&tokenizer, &request.messages, arch_hint.as_deref(), request.thinking(), state); + tokenize_chat_prompt(&tokenizer, &request.messages, arch_hint.as_deref(), request.thinking(), request.tools.as_deref(), state); let prompt_ids = match fit_serving_context(state, tokenized) { Ok(ids) => ids, @@ -152,49 +153,10 @@ async fn try_cuda_backend( let (timing_tx, timing_rx) = tokio::sync::oneshot::channel::(); - // PMAT-044: Use batch scheduler if available (continuous batching) - if let Some(batch_tx) = state.cuda_batch_tx() { - let batch_req = super::cuda_batch_scheduler::CudaBatchRequest { - prompt_ids, - config: q_config, - token_tx: tx, - non_streaming: false, - enqueue_time: std::time::Instant::now(), - timing_tx: Some(timing_tx), - }; - if let Err(e) = batch_tx.try_send(batch_req) { - // §5.2: this 503 is the one admission REFUSAL this server has - // (the policy is otherwise `queue`), so it is counted where it - // is returned — `kv.admission_rejected` reads this counter. - state.record_admission_rejected(); - return Some(fail_response( - state, - StatusCode::SERVICE_UNAVAILABLE, - format!("Batch queue full: {e}"), - )); - } - } else { - // Fallback: direct RwLock path (serialized) - let cuda_model_clone = cuda_model_lock.clone(); - let prompt_ids_clone = prompt_ids.clone(); - let q_config_clone = q_config.clone(); - let sink_metrics = state.metrics.clone(); - - tokio::task::spawn_blocking(move || { - let mut cuda_model = cuda_model_clone.write().expect("operation failed"); - let generate_start = std::time::Instant::now(); - // Stops when the client goes away — see `streaming_token_sink`. - let sink = - crate::api::openai_handlers::streaming_token_sink(tx.clone(), sink_metrics); - let result = - dense_cuda_turn(&mut cuda_model, &prompt_ids_clone, &q_config_clone, sink); - // Taken under the SAME write lock the request ran under, so the - // split belongs to this request and to no other. - let _ = timing_tx.send(phase_split(&mut cuda_model, generate_start)); - if let Err(e) = result { - let _ = tx.blocking_send(Err(e.to_string())); - } - }); + if let Err(r) = + dispatch_cuda_stream(state, cuda_model_lock, prompt_ids, q_config, tx, timing_tx) + { + return Some(r); } return Some(true_streaming_sse_response( @@ -213,54 +175,14 @@ async fn try_cuda_backend( // Non-streaming CUDA — route through batch scheduler when available (realizr#211) let (timing_tx, timing_rx) = tokio::sync::oneshot::channel::(); - let (token_ids, completion_tokens, response_text) = if let Some(batch_tx) = state.cuda_batch_tx() { - // Use batch scheduler: submit request and collect all tokens - // realizr#212: capacity 512 for bulk-send after non-streaming generation - let (tx, mut rx) = tokio::sync::mpsc::channel::>(512); - let batch_req = super::cuda_batch_scheduler::CudaBatchRequest { - prompt_ids, - config: q_config, - token_tx: tx, - non_streaming: true, // realizr#212: scheduler accumulates + bulk-sends - enqueue_time: std::time::Instant::now(), - timing_tx: Some(timing_tx), - }; - if let Err(e) = batch_tx.try_send(batch_req) { - // §5.2: counted at the refusal, as above. - state.record_admission_rejected(); - return Some(fail_response( - state, - StatusCode::SERVICE_UNAVAILABLE, - format!("Batch queue full: {e}"), - )); - } - // Collect all tokens via async receive (realizr#211) - let mut tokens = Vec::new(); - while let Some(result) = rx.recv().await { - match result { - Ok(token_id) => tokens.push(token_id), - // D5: `fit_serving_context` above already refused an over-context - // prompt with a 400; the scheduler's errors are strings, and what - // reaches here is a server fault. - Err(e) => return Some(fail_response(state, StatusCode::INTERNAL_SERVER_ERROR, e)), - } - } - let n = tokens.len(); - let text = tokenizer.decode(&tokens).unwrap_or_else(|_| String::new()); - (tokens, n, clean_chat_output(&text)) + let generated = if let Some(batch_tx) = state.cuda_batch_tx() { + cuda_batch_collect(state, batch_tx, prompt_ids, q_config, timing_tx, &tokenizer).await } else { - // Fallback: direct RwLock path (serialized, no batch scheduler) - let mut cuda_model = cuda_model_lock.write().expect("operation failed"); - let generate_start = std::time::Instant::now(); - let generated = match dense_cuda_turn(&mut cuda_model, &prompt_ids, &q_config, |_| true) { - Ok(g) => g, - Err(e) => return Some(fail_response(state, crate::api::generation_error_status(&e), e)), - }; - let _ = timing_tx.send(phase_split(&mut cuda_model, generate_start)); - let tokens: Vec = generated.iter().skip(prompt_tokens).copied().collect(); - let n = tokens.len(); - let text = tokenizer.decode(&tokens).unwrap_or_else(|_| String::new()); - (tokens, n, clean_chat_output(&text)) + cuda_direct_collect(state, cuda_model_lock, &prompt_ids, &q_config, timing_tx, &tokenizer, prompt_tokens) + }; + let (token_ids, completion_tokens, response_text) = match generated { + Ok(g) => g, + Err(r) => return Some(r), }; let latency = start.elapsed(); @@ -287,6 +209,137 @@ async fn try_cuda_backend( )) } +/// `try_cuda_backend`'s streaming dispatch: the batch scheduler when one is +/// running, else the direct RwLock path on a blocking task. `Err` is the 503 +/// for a full batch queue. +#[cfg(feature = "cuda")] +#[allow(clippy::result_large_err)] +fn dispatch_cuda_stream( + state: &AppState, + cuda_model_lock: &Arc>, + prompt_ids: Vec, + q_config: crate::gguf::QuantizedGenerateConfig, + tx: tokio::sync::mpsc::Sender>, + timing_tx: tokio::sync::oneshot::Sender, +) -> Result<(), Response> { + // PMAT-044: Use batch scheduler if available (continuous batching) + if let Some(batch_tx) = state.cuda_batch_tx() { + let batch_req = super::cuda_batch_scheduler::CudaBatchRequest { + prompt_ids, + config: q_config, + token_tx: tx, + non_streaming: false, + enqueue_time: std::time::Instant::now(), + timing_tx: Some(timing_tx), + }; + if let Err(e) = batch_tx.try_send(batch_req) { + // §5.2: this 503 is the one admission REFUSAL this server has + // (the policy is otherwise `queue`), so it is counted where it + // is returned — `kv.admission_rejected` reads this counter. + state.record_admission_rejected(); + return Err(fail_response( + state, + StatusCode::SERVICE_UNAVAILABLE, + format!("Batch queue full: {e}"), + )); + } + } else { + // Fallback: direct RwLock path (serialized) + let cuda_model_clone = cuda_model_lock.clone(); + let sink_metrics = state.metrics.clone(); + + tokio::task::spawn_blocking(move || { + let mut cuda_model = cuda_model_clone.write().expect("operation failed"); + let generate_start = std::time::Instant::now(); + // Stops when the client goes away — see `streaming_token_sink`. + let sink = + crate::api::openai_handlers::streaming_token_sink(tx.clone(), sink_metrics); + let result = dense_cuda_turn(&mut cuda_model, &prompt_ids, &q_config, sink); + // Taken under the SAME write lock the request ran under, so the + // split belongs to this request and to no other. + let _ = timing_tx.send(phase_split(&mut cuda_model, generate_start)); + if let Err(e) = result { + let _ = tx.blocking_send(Err(e.to_string())); + } + }); + } + Ok(()) +} + +/// `try_cuda_backend`'s non-streaming turn through the batch scheduler +/// (realizr#211): submit, then collect every token. Returns +/// `(tokens, completion_tokens, cleaned text)`. +#[cfg(feature = "cuda")] +async fn cuda_batch_collect( + state: &AppState, + batch_tx: &tokio::sync::mpsc::Sender, + prompt_ids: Vec, + q_config: crate::gguf::QuantizedGenerateConfig, + timing_tx: tokio::sync::oneshot::Sender, + tokenizer: &BPETokenizer, +) -> Result<(Vec, usize, String), Response> { + // Use batch scheduler: submit request and collect all tokens + // realizr#212: capacity 512 for bulk-send after non-streaming generation + let (tx, mut rx) = tokio::sync::mpsc::channel::>(512); + let batch_req = super::cuda_batch_scheduler::CudaBatchRequest { + prompt_ids, + config: q_config, + token_tx: tx, + non_streaming: true, // realizr#212: scheduler accumulates + bulk-sends + enqueue_time: std::time::Instant::now(), + timing_tx: Some(timing_tx), + }; + if let Err(e) = batch_tx.try_send(batch_req) { + // §5.2: counted at the refusal, as in `dispatch_cuda_stream`. + state.record_admission_rejected(); + return Err(fail_response( + state, + StatusCode::SERVICE_UNAVAILABLE, + format!("Batch queue full: {e}"), + )); + } + // Collect all tokens via async receive (realizr#211) + let mut tokens = Vec::new(); + while let Some(result) = rx.recv().await { + match result { + Ok(token_id) => tokens.push(token_id), + // D5: `fit_serving_context` already refused an over-context + // prompt with a 400; the scheduler's errors are strings, and what + // reaches here is a server fault. + Err(e) => return Err(fail_response(state, StatusCode::INTERNAL_SERVER_ERROR, e)), + } + } + let n = tokens.len(); + let text = tokenizer.decode(&tokens).unwrap_or_else(|_| String::new()); + Ok((tokens, n, clean_chat_output(&text))) +} + +/// `try_cuda_backend`'s non-streaming turn when no batch scheduler runs: the +/// direct RwLock path (serialized). Same return shape as `cuda_batch_collect`. +#[cfg(feature = "cuda")] +#[allow(clippy::result_large_err)] +fn cuda_direct_collect( + state: &AppState, + cuda_model_lock: &Arc>, + prompt_ids: &[u32], + q_config: &crate::gguf::QuantizedGenerateConfig, + timing_tx: tokio::sync::oneshot::Sender, + tokenizer: &BPETokenizer, + prompt_tokens: usize, +) -> Result<(Vec, usize, String), Response> { + let mut cuda_model = cuda_model_lock.write().expect("operation failed"); + let generate_start = std::time::Instant::now(); + let generated = match dense_cuda_turn(&mut cuda_model, prompt_ids, q_config, |_| true) { + Ok(g) => g, + Err(e) => return Err(fail_response(state, crate::api::generation_error_status(&e), e)), + }; + let _ = timing_tx.send(phase_split(&mut cuda_model, generate_start)); + let tokens: Vec = generated.iter().skip(prompt_tokens).copied().collect(); + let n = tokens.len(); + let text = tokenizer.decode(&tokens).unwrap_or_else(|_| String::new()); + Ok((tokens, n, clean_chat_output(&text))) +} + /// §3: pair the engine's prefill measurement with the decode remainder. /// /// `take_phase_timings` clears as it reads, so a request that took a @@ -358,7 +411,7 @@ fn try_quantized_backend( // GH-319: Use actual model architecture for chat template detection let arch_hint = state.model_architecture(); let prompt_ids = - match tokenize_chat_prompt(&tokenizer, &request.messages, arch_hint.as_deref(), request.thinking(), state) { + match tokenize_chat_prompt(&tokenizer, &request.messages, arch_hint.as_deref(), request.thinking(), request.tools.as_deref(), state) { Ok(ids) => ids, Err(r) => return Some(r), }; @@ -516,7 +569,7 @@ fn try_apr_transformer_backend( }; let arch_hint = state.model_architecture(); let prompt_ids = - match tokenize_chat_prompt(&tokenizer, &request.messages, arch_hint.as_deref(), request.thinking(), state) { + match tokenize_chat_prompt(&tokenizer, &request.messages, arch_hint.as_deref(), request.thinking(), request.tools.as_deref(), state) { Ok(ids) => ids, Err(r) => return Some(r), }; @@ -613,11 +666,12 @@ fn registry_prompt_ids( request: &ChatCompletionRequest, tokenizer: &BPETokenizer, ) -> Result, Response> { - let prompt_text = match crate::api::realize_handlers::format_chat_messages_for_state_thinking( + let prompt_text = match crate::api::realize_handlers::format_chat_messages_for_state_thinking_tools( state, &request.messages, Some(&request.model), request.thinking(), + request.tools.as_deref(), ) { Ok(p) => p, Err(e) => return Err(fail_response(state, StatusCode::BAD_REQUEST, e.to_string())), @@ -820,7 +874,7 @@ async fn try_apr_q4k_chat_backend( }; let arch_hint = state.model_architecture(); let prompt_ids = - match tokenize_chat_prompt(&tokenizer, &request.messages, arch_hint.as_deref(), request.thinking(), state) { + match tokenize_chat_prompt(&tokenizer, &request.messages, arch_hint.as_deref(), request.thinking(), request.tools.as_deref(), state) { Ok(ids) => ids, Err(r) => return Some(r), }; @@ -1229,6 +1283,7 @@ fn try_qwen3_moe_backend( &request.messages, Some(&request.model), request.thinking(), + request.tools.as_deref(), state, ) { Ok(ids) => ids, diff --git a/crates/aprender-serve/src/api/gpu_completions_handler.rs b/crates/aprender-serve/src/api/gpu_completions_handler.rs index 3965052262..14c1ad0cc1 100644 --- a/crates/aprender-serve/src/api/gpu_completions_handler.rs +++ b/crates/aprender-serve/src/api/gpu_completions_handler.rs @@ -598,10 +598,33 @@ async fn completions_inner( return Ok(r); } - // GH-627/637/670: Direct CUDA model fallback for APR GPU path - // with_cuda_model_and_vocab sets cuda_model but NOT model, - // so registry_completions fails with "No model available". #[cfg(feature = "cuda")] + if let Some(r) = try_cuda_direct_completions(&state, &request, max_tokens, temperature, start)? { + return Ok(r); + } + + // aprender#2609: the f32 APR / SafeTensors CPU backend, in the same position + // the chat chain puts it — after quantized, before the dense registry. + if let Some(r) = + try_apr_transformer_completions(&state, &request, max_tokens, temperature, start, &cancel)? + { + return Ok(r); + } + + registry_completions(&state, &request, max_tokens, temperature, start, &cancel) +} + +/// GH-627/637/670: Direct CUDA model fallback for APR GPU path. +/// with_cuda_model_and_vocab sets cuda_model but NOT model, +/// so registry_completions fails with "No model available". +#[cfg(feature = "cuda")] +fn try_cuda_direct_completions( + state: &AppState, + request: &CompletionRequest, + max_tokens: usize, + temperature: f32, + start: std::time::Instant, +) -> Result, RErr> { if let Some(cuda_lock) = state.cuda_model() { use crate::gguf::QuantizedGenerateConfig; let tokenizer = state.tokenizer.clone().ok_or_else(|| { @@ -636,7 +659,7 @@ async fn completions_inner( // #2465(2): this inline backend ignored `request.stop` too. let (text, finish_reason) = apply_stop_sequences(text, request.stop.as_deref(), completion_tokens, max_tokens); - return Ok(CompletionResponse { + return Ok(Some(CompletionResponse { id: format!("cmpl-cuda-{}", elapsed.as_millis()), object: "text_completion".to_string(), created: std::time::SystemTime::now() @@ -657,18 +680,9 @@ async fn completions_inner( }, // The inline CUDA block records no backend flag either (#3894). used_gpu: None, - }); + })); } - - // aprender#2609: the f32 APR / SafeTensors CPU backend, in the same position - // the chat chain puts it — after quantized, before the dense registry. - if let Some(r) = - try_apr_transformer_completions(&state, &request, max_tokens, temperature, start, &cancel)? - { - return Ok(r); - } - - registry_completions(&state, &request, max_tokens, temperature, start, &cancel) + Ok(None) } /// The `/v1/logprobs` generation config: greedy, logprobs on, and stopping on diff --git a/crates/aprender-serve/src/api/mod.rs b/crates/aprender-serve/src/api/mod.rs index a51c76bd10..db6007a969 100644 --- a/crates/aprender-serve/src/api/mod.rs +++ b/crates/aprender-serve/src/api/mod.rs @@ -122,9 +122,11 @@ pub use gpu_handlers::{spawn_batch_processor, BatchConfig}; mod realize_handlers; pub(crate) use realize_handlers::{ clean_chat_output, format_chat_messages, format_chat_messages_for_state, - format_chat_messages_for_state_thinking, format_chat_messages_official, - format_chat_messages_official_thinking, openai_completions_handler, openai_embeddings_handler, - realize_embed_handler, realize_model_handler, realize_reload_handler, + format_chat_messages_for_state_thinking, format_chat_messages_for_state_thinking_tools, + format_chat_messages_official, format_chat_messages_official_thinking, + format_chat_messages_official_thinking_tools, openai_completions_handler, + openai_embeddings_handler, realize_embed_handler, realize_model_handler, + realize_reload_handler, }; #[cfg(feature = "cuda")] pub(crate) use realize_handlers::{logprobs_handler, perplexity_handler}; diff --git a/crates/aprender-serve/src/api/mod_create_demo.rs b/crates/aprender-serve/src/api/mod_create_demo.rs index 0036602a7f..8ebddb2654 100644 --- a/crates/aprender-serve/src/api/mod_create_demo.rs +++ b/crates/aprender-serve/src/api/mod_create_demo.rs @@ -383,10 +383,24 @@ fn extract_tool_parameters( .and_then(|d| d.as_str()) .unwrap_or_default() .to_string(); + // #4650: the scalar JSON-Schema type, so a call parsed from text (Qwen XML) + // types its value as declared. Anything else stays String, as before. + let param_type = match spec.get("type").and_then(|t| t.as_str()) { + Some("integer") => ToolParameterType::Integer, + Some("number") => ToolParameterType::Number, + Some("boolean") => ToolParameterType::Boolean, + Some("array") => ToolParameterType::Array { + items: Box::new(ToolParameterType::String), + }, + Some("object") => ToolParameterType::Object { + properties: Vec::new(), + }, + _ => ToolParameterType::String, + }; ToolParameter { name: name.clone(), description, - param_type: ToolParameterType::String, + param_type, required: required.contains(name.as_str()), default: None, } diff --git a/crates/aprender-serve/src/api/openai_handlers.rs b/crates/aprender-serve/src/api/openai_handlers.rs index 01f580407e..4d7ea9fed8 100644 --- a/crates/aprender-serve/src/api/openai_handlers.rs +++ b/crates/aprender-serve/src/api/openai_handlers.rs @@ -21,7 +21,7 @@ use futures::stream::Stream; use super::{ build_trace_data, clean_chat_output, format_chat_messages, - format_chat_messages_for_state_thinking, AppState, ChatChoice, ChatCompletionChunk, + format_chat_messages_for_state_thinking_tools, AppState, ChatChoice, ChatCompletionChunk, ChatCompletionRequest, ChatCompletionResponse, ChatMessage, ErrorResponse, FinishReason, OpenAIModel, OpenAIModelsResponse, StreamMode, Usage, }; @@ -90,12 +90,13 @@ fn tokenize_chat_prompt( messages: &[ChatMessage], model_hint: Option<&str>, thinking: Option, + tools: Option<&[super::OpenAiTool]>, state: &AppState, ) -> Result, Response> { // #3723: the request's thinking mode; an ON the model's template cannot express is the - // client's error, answered by name. + // client's error, answered by name. #4650: the request's tools reach the template too. let prompt_text = - format_chat_messages_for_state_thinking(state, messages, model_hint, thinking) + format_chat_messages_for_state_thinking_tools(state, messages, model_hint, thinking, tools) .map_err(|e| fail_response(state, StatusCode::BAD_REQUEST, e.to_string()))?; let ids = tokenizer.encode(&prompt_text); if ids.is_empty() { @@ -639,8 +640,21 @@ fn build_tool_calling_message( let defs: Vec = tools.iter().map(super::OpenAiTool::to_grammar).collect(); - let mut parser = ToolCallParser::new(defs); - let calls = parser.parse(&text); + // #4650: Qwen3.5's template instructs `` XML. When the + // text carries that marker the XML parser goes FIRST: a parameter value may hold + // JSON shaped like a tool call, and the default parser would take that inner object + // as the call. With the marker the XML result is FINAL, so an XML call to an + // undeclared tool stays content rather than falling back to the JSON inside it. The + // price: a JSON call whose string argument quotes a whole declared `` + // block resolves to that XML call, and JSON in text that only mentions the marker + // is not read. + let calls = if text.contains(" ids, @@ -1188,6 +1203,7 @@ fn try_cached_backend( &request.messages, arch_hint.as_deref(), request.thinking(), + request.tools.as_deref(), state, ) { Ok(ids) => ids, @@ -1426,6 +1442,109 @@ mod pmat801_tool_calling_tests { assert_eq!(msg.role, "assistant"); } + fn args_of(msg: &ChatMessage) -> serde_json::Value { + let calls = msg.tool_calls.as_ref().expect("tool_calls populated"); + assert_eq!(calls.len(), 1); + serde_json::from_str(&calls[0].function.arguments).expect("arguments is JSON") + } + + /// #4650 must-RED on main: Qwen3.5's template tells the model to call in XML, and the + /// parser read only JSON, so a correct call came back as plain content. + #[test] + fn qwen35_xml_tool_call_populates_tool_calls_4650() { + let tools = vec![weather_tool()]; + let generated = "I'll check.\n\n\n\n\nParis\n\n\n".to_string(); + let (msg, reason) = build_tool_calling_message(generated, "stop".to_string(), &tools, None); + assert_eq!(reason, "tool_calls"); + assert_eq!( + msg.tool_calls.as_ref().expect("calls")[0].function.name, + "get_weather" + ); + assert_eq!(args_of(&msg), serde_json::json!({"city": "Paris"})); + } + + /// Values are typed by the tool's schema: an integer parameter is a JSON number, a + /// string parameter stays the string the model wrote even when it looks numeric. + #[test] + fn qwen35_xml_values_follow_the_schema_types_4650() { + let tool = OpenAiTool { + tool_type: "function".to_string(), + function: OpenAiFunctionDef { + name: "lookup".to_string(), + description: "Lookup".to_string(), + parameters: Some(serde_json::json!({ + "type": "object", + "properties": { "zip": { "type": "string" }, "days": { "type": "integer" } }, + "required": ["zip", "days"], + })), + }, + }; + let generated = "\n\n\n02134\n\n\n3\n\n\n".to_string(); + let (msg, reason) = + build_tool_calling_message(generated, "stop".to_string(), &[tool], None); + assert_eq!(reason, "tool_calls"); + assert_eq!( + args_of(&msg), + serde_json::json!({"zip": "02134", "days": 3}) + ); + } + + /// must-RED with JSON parsed first: an XML call whose parameter value is itself a + /// JSON tool-call object. The default parser took the inner object as the call and + /// the real XML call was lost. + #[test] + fn qwen35_xml_call_wins_over_json_inside_a_parameter_4650() { + let note = OpenAiTool { + tool_type: "function".to_string(), + function: OpenAiFunctionDef { + name: "save_note".to_string(), + description: "Save a note".to_string(), + parameters: Some(serde_json::json!({ + "type": "object", + "properties": { "body": { "type": "string" } }, + "required": ["body"], + })), + }, + }; + let tools = vec![weather_tool(), note]; + let inner = r#"{"name": "get_weather", "arguments": {"city": "NYC"}}"#; + let generated = format!( + "\n\n\n{inner}\n\n\n" + ); + let (msg, reason) = build_tool_calling_message(generated, "stop".to_string(), &tools, None); + assert_eq!(reason, "tool_calls"); + assert_eq!( + msg.tool_calls.as_ref().expect("calls")[0].function.name, + "save_note" + ); + assert_eq!(args_of(&msg), serde_json::json!({"body": inner})); + } + + /// must-RED with a JSON fallback after XML: an XML call to an undeclared tool parses to + /// nothing, and the fallback took the JSON in its parameter as a declared call. + #[test] + fn qwen35_undeclared_xml_call_does_not_promote_inner_json_4650() { + let tools = vec![weather_tool()]; + let generated = "\n\n\n{\"name\": \"get_weather\", \"arguments\": {\"city\": \"NYC\"}}\n\n\n".to_string(); + let (msg, reason) = + build_tool_calling_message(generated.clone(), "stop".to_string(), &tools, None); + assert_eq!(reason, "stop"); + assert!(msg.tool_calls.is_none()); + assert_eq!(msg.content, generated); + } + + /// A call to a tool the request never declared is not a tool call. + #[test] + fn qwen35_xml_call_to_an_undeclared_tool_stays_content_4650() { + let tools = vec![weather_tool()]; + let generated = "\n\n\n/\n\n\n".to_string(); + let (msg, reason) = + build_tool_calling_message(generated.clone(), "stop".to_string(), &tools, None); + assert_eq!(reason, "stop"); + assert!(msg.tool_calls.is_none()); + assert_eq!(msg.content, generated); + } + /// tool_choice "none" skips parsing even when a tool call is present in text. #[test] fn tool_choice_none_skips_parsing() { @@ -1563,7 +1682,8 @@ mod chat_template_wiring_3990 { official, legacy, "the probe must distinguish the two renders" ); - let got = tokenize_chat_prompt(&tokenizer, &msgs, None, None, &state).expect("tokenizes"); + let got = + tokenize_chat_prompt(&tokenizer, &msgs, None, None, None, &state).expect("tokenizes"); assert_eq!( got, tokenizer.encode(&official), @@ -1604,8 +1724,9 @@ mod chat_template_wiring_3990 { official, "the probe must distinguish ({client_model})" ); - let got = tokenize_chat_prompt(&tokenizer, &msgs, Some(client_model), None, &state) - .expect("tokenizes"); + let got = + tokenize_chat_prompt(&tokenizer, &msgs, Some(client_model), None, None, &state) + .expect("tokenizes"); assert_eq!( got, tokenizer.encode(&official), @@ -1706,8 +1827,8 @@ mod thinking_on_refusal_3723 { content: "Hi".to_string(), ..Default::default() }]; - assert!(tokenize_chat_prompt(&tokenizer, &msgs, None, Some(false), &state).is_ok()); - let refused = tokenize_chat_prompt(&tokenizer, &msgs, None, Some(true), &state); + assert!(tokenize_chat_prompt(&tokenizer, &msgs, None, Some(false), None, &state).is_ok()); + let refused = tokenize_chat_prompt(&tokenizer, &msgs, None, Some(true), None, &state); assert!( refused.is_err(), "Qwen2.5's template renders ON == OFF; ON must be refused" diff --git a/crates/aprender-serve/src/api/qwen35_chat_backend.rs b/crates/aprender-serve/src/api/qwen35_chat_backend.rs index bf24311418..164999c9bb 100644 --- a/crates/aprender-serve/src/api/qwen35_chat_backend.rs +++ b/crates/aprender-serve/src/api/qwen35_chat_backend.rs @@ -106,15 +106,19 @@ async fn try_qwen35_backend( let architecture = state.model_architecture(); // #3723: the request's thinking mode, rendered by the model's own template. - let prompt_text = match crate::api::realize_handlers::format_chat_messages_official_thinking( - Some(&mapped.model), - &request.messages, - architecture.as_deref(), - request.thinking(), - ) { - Ok(p) => p, - Err(e) => return Some(fail_response(state, StatusCode::BAD_REQUEST, e.to_string())), - }; + // #4650: and the request's tools -- the template's `# Tools` block is the only place the + // model learns a tool exists and the `` format to call it in. + let prompt_text = + match crate::api::realize_handlers::format_chat_messages_official_thinking_tools( + Some(&mapped.model), + &request.messages, + architecture.as_deref(), + request.thinking(), + request.tools.as_deref(), + ) { + Ok(p) => p, + Err(e) => return Some(fail_response(state, StatusCode::BAD_REQUEST, e.to_string())), + }; let input_ids = mapped.model.encode(&prompt_text).unwrap_or_default(); if input_ids.is_empty() { return Some(fail_response( diff --git a/crates/aprender-serve/src/api/realize_handlers.rs b/crates/aprender-serve/src/api/realize_handlers.rs index 2b4bff57e1..0f0410e09f 100644 --- a/crates/aprender-serve/src/api/realize_handlers.rs +++ b/crates/aprender-serve/src/api/realize_handlers.rs @@ -247,6 +247,23 @@ pub fn format_chat_messages_official_thinking( messages: &[ChatMessage], model_hint: Option<&str>, thinking: Option, +) -> Result { + format_chat_messages_official_thinking_tools(gguf, messages, model_hint, thinking, None) +} + +/// #4650: [`format_chat_messages_official_thinking`] with the request's `tools`, rendered +/// by the model's own template (its `# Tools` block). An empty array is no tools, as in +/// OpenAI. The hand-coded family templates have no tools block, so a request with tools +/// that falls back to one says so on stderr instead of dropping them in silence. +/// +/// # Errors +/// See [`format_chat_messages_official_thinking`]. +pub fn format_chat_messages_official_thinking_tools( + gguf: Option<&crate::gguf::GGUFModel>, + messages: &[ChatMessage], + model_hint: Option<&str>, + thinking: Option, + tools: Option<&[super::OpenAiTool]>, ) -> Result { use crate::chat_template::{self, ChatMessage as TemplateMessage}; @@ -254,13 +271,28 @@ pub fn format_chat_messages_official_thinking( .iter() .map(|m| TemplateMessage::new(&m.role, &m.content)) .collect(); + let tools_json = tools + .filter(|t| !t.is_empty()) + .and_then(|t| serde_json::to_value(t).ok()); let own = gguf .filter(|g| g.metadata.contains_key("tokenizer.chat_template")) .map(|g| { - let msgs = &template_messages; - move |t: Option| chat_template::render_official_for_model(g, msgs, t) + let (msgs, tools) = (&template_messages, tools_json.as_ref()); + move |t: Option| { + chat_template::render_official_for_model_with_tools(g, msgs, t, tools) + } }); - chat_template::official_or_legacy(own, || format_chat_messages(messages, model_hint), thinking) + let legacy = || { + if tools_json.is_some() { + eprintln!( + "warning: this request carries tools, and the model's own chat template is not \ + being used; apr's built-in template has no tools block, so the model is not \ + told about them (#4650)" + ); + } + format_chat_messages(messages, model_hint) + }; + chat_template::official_or_legacy(own, legacy, thinking) } /// [`format_chat_messages_official`] against whatever GGUF the server retained. @@ -287,15 +319,30 @@ pub fn format_chat_messages_for_state_thinking( messages: &[ChatMessage], model_hint: Option<&str>, thinking: Option, +) -> Result { + format_chat_messages_for_state_thinking_tools(state, messages, model_hint, thinking, None) +} + +/// #4650: [`format_chat_messages_for_state_thinking`] with the request's `tools`. +/// +/// # Errors +/// See [`format_chat_messages_official_thinking`]. +pub fn format_chat_messages_for_state_thinking_tools( + state: &AppState, + messages: &[ChatMessage], + model_hint: Option<&str>, + thinking: Option, + tools: Option<&[super::OpenAiTool]>, ) -> Result { let mapped = state.mapped_gguf_model(); let architecture = state.model_architecture(); let hint = architecture.as_deref().or(model_hint); - format_chat_messages_official_thinking( + format_chat_messages_official_thinking_tools( mapped.as_ref().map(|m| &m.model), messages, hint, thinking, + tools, ) } diff --git a/crates/aprender-serve/src/api/realize_handlers_official_3990_tests.rs b/crates/aprender-serve/src/api/realize_handlers_official_3990_tests.rs index d831b447fa..9d116d5e33 100644 --- a/crates/aprender-serve/src/api/realize_handlers_official_3990_tests.rs +++ b/crates/aprender-serve/src/api/realize_handlers_official_3990_tests.rs @@ -45,6 +45,41 @@ mod serve_official_chat_template_3990 { assert_eq!(got, "be tersehi"); } + /// #4650 mutant kill (`!t.is_empty()` at realize_handlers.rs:275): the request's tools + /// reach the model's own template, and an empty array is no tools. + #[test] + fn request_tools_reach_the_ggufs_own_template_4650() { + use crate::api::{OpenAiFunctionDef, OpenAiTool}; + let g = gguf_with(Some( + "{% if tools %}T:{{ tools[0]['function']['name'] }}{% else %}NOTOOLS{% endif %}|{% for m in messages %}{{ m['content'] }}{% endfor %}", + )); + let tool = OpenAiTool { + tool_type: "function".to_string(), + function: OpenAiFunctionDef { + name: "get_weather".to_string(), + description: "Get the weather".to_string(), + parameters: None, + }, + }; + let msgs = [msg("user", "hi")]; + let render = |tools: Option<&[OpenAiTool]>| { + super::format_chat_messages_official_thinking_tools( + Some(&g), + &msgs, + Some("qwen2"), + Some(false), + tools, + ) + .expect("renders") + }; + assert_eq!( + render(Some(std::slice::from_ref(&tool))), + "T:get_weather|hi" + ); + assert_eq!(render(Some(&[])), "NOTOOLS|hi"); + assert_eq!(render(None), "NOTOOLS|hi"); + } + #[test] fn no_gguf_or_no_template_keeps_the_legacy_formatter_3990() { let msgs = [msg("user", "hi")]; diff --git a/crates/aprender-serve/src/api/tests/dense_session_4268.rs b/crates/aprender-serve/src/api/tests/dense_session_4268.rs index 755bb0cdde..2db28d6366 100644 --- a/crates/aprender-serve/src/api/tests/dense_session_4268.rs +++ b/crates/aprender-serve/src/api/tests/dense_session_4268.rs @@ -50,7 +50,7 @@ fn prompt_ids(state: &AppState, body: &serde_json::Value) -> Vec { serde_json::from_value(body.clone()).expect("a chat request"); let tokenizer = require_tokenizer(state).unwrap_or_else(|_| panic!("tokenizer")); let arch = state.model_architecture(); - tokenize_chat_prompt(&tokenizer, &request.messages, arch.as_deref(), None, state) + tokenize_chat_prompt(&tokenizer, &request.messages, arch.as_deref(), None, None, state) .unwrap_or_else(|_| panic!("prompt ids")) } diff --git a/crates/aprender-serve/src/chat_template_official.rs b/crates/aprender-serve/src/chat_template_official.rs index 24f3bc452a..ba3b27d148 100644 --- a/crates/aprender-serve/src/chat_template_official.rs +++ b/crates/aprender-serve/src/chat_template_official.rs @@ -123,6 +123,63 @@ fn str_split(s: &str, args: &[minijinja::Value]) -> Result( + &mut self, + writer: &mut W, + first: bool, + ) -> std::io::Result<()> { + if first { + Ok(()) + } else { + writer.write_all(b", ") + } + } + + fn begin_object_key( + &mut self, + writer: &mut W, + first: bool, + ) -> std::io::Result<()> { + if first { + Ok(()) + } else { + writer.write_all(b", ") + } + } + + fn begin_object_value( + &mut self, + writer: &mut W, + ) -> std::io::Result<()> { + writer.write_all(b": ") + } +} + +/// The `tojson` filter as HuggingFace defines it for chat templates (#4650): +/// `json.dumps(x, ensure_ascii=False)`. minijinja's own `tojson` writes compact separators +/// and HTML-escapes `<`, `>`, `&` and `'` to `<`-style escapes, so a tool schema that +/// says "a < b" reached the model as text it was never trained on. The `tools` block and +/// replayed tool-call arguments are the only places the Qwen templates call it. +fn py_tojson(value: &minijinja::Value) -> Result { + use serde::Serialize; + let mut out = Vec::new(); + let mut ser = serde_json::Serializer::with_formatter(&mut out, PyJsonFormatter); + value.serialize(&mut ser).map_err(|e| { + minijinja::Error::new( + minijinja::ErrorKind::BadSerialization, + format!("tojson: {e}"), + ) + })?; + let s = String::from_utf8(out).map_err(|e| { + minijinja::Error::new(minijinja::ErrorKind::BadSerialization, e.to_string()) + })?; + Ok(minijinja::Value::from_safe_string(s)) +} + /// Render a model's own jinja chat template, with llama.cpp/HuggingFace semantics (#3990). /// /// # Errors @@ -134,12 +191,44 @@ pub fn render_official( messages: &[ChatMessage], add_generation_prompt: bool, enable_thinking: Option, +) -> Result { + render_official_with_tools( + chat_template, + bos_token, + eos_token, + messages, + add_generation_prompt, + enable_thinking, + None, + ) +} + +/// [`render_official`] with the request's OpenAI `tools` array (#4650). +/// +/// Qwen2.5, Qwen3 and Qwen3.5 templates gate their whole `# Tools` system block on +/// `{% if tools %}`. The context used to carry no `tools` key, and minijinja renders an +/// UNDEFINED variable as false with no error -- so every tool a client sent was dropped +/// from the prompt, silently, and the model was never told a tool existed. `tools` is the +/// array as the client sent it (`[{"type":"function","function":{...}}]`), the shape +/// HuggingFace's `apply_chat_template(tools=...)` passes; `None` leaves it undefined. +/// +/// # Errors +/// See [`render_official`]. +pub fn render_official_with_tools( + chat_template: &str, + bos_token: Option<&str>, + eos_token: Option<&str>, + messages: &[ChatMessage], + add_generation_prompt: bool, + enable_thinking: Option, + tools: Option<&serde_json::Value>, ) -> Result { let mut env = Environment::new(); env.set_recursion_limit(MAX_RECURSION_DEPTH); env.set_trim_blocks(true); env.set_lstrip_blocks(true); env.set_unknown_method_callback(official_template_str_method); + env.add_filter("tojson", py_tojson); env.add_function( "raise_exception", |msg: String| -> Result { @@ -173,6 +262,9 @@ pub fn render_official( if let Some(t) = enable_thinking { ctx.insert("enable_thinking", minijinja::Value::from(t)); } + if let Some(t) = tools { + ctx.insert("tools", minijinja::Value::from_serialize(t)); + } tmpl.render(minijinja::Value::from(ctx)) .map_err(|e| RealizarError::FormatError { reason: format!("model chat_template failed to render: {e}"), @@ -189,6 +281,20 @@ pub fn render_official_for_model( gguf: &crate::gguf::GGUFModel, messages: &[ChatMessage], enable_thinking: Option, +) -> Result { + render_official_for_model_with_tools(gguf, messages, enable_thinking, None) +} + +/// [`render_official_for_model`] with the request's `tools` (#4650); see +/// [`render_official_with_tools`]. +/// +/// # Errors +/// See [`render_official_for_model`]. +pub fn render_official_for_model_with_tools( + gguf: &crate::gguf::GGUFModel, + messages: &[ChatMessage], + enable_thinking: Option, + tools: Option<&serde_json::Value>, ) -> Result { let Some(crate::gguf::GGUFValue::String(tpl)) = gguf.metadata.get("tokenizer.chat_template") else { @@ -202,13 +308,14 @@ pub fn render_official_for_model( v.get(usize::try_from(i).ok()?).cloned() }; let (bos, eos) = (piece(gguf.bos_token_id()), piece(gguf.eos_token_id())); - render_official( + render_official_with_tools( tpl, bos.as_deref(), eos.as_deref(), messages, true, enable_thinking, + tools, ) } diff --git a/crates/aprender-serve/src/chat_template_official_tests.rs b/crates/aprender-serve/src/chat_template_official_tests.rs index 6eda81acf7..482560e49e 100644 --- a/crates/aprender-serve/src/chat_template_official_tests.rs +++ b/crates/aprender-serve/src/chat_template_official_tests.rs @@ -216,3 +216,95 @@ mod official_chat_template_3990 { ); } } + +// #4650: a request's OpenAI `tools` must reach the model's own template. The context used +// to carry no `tools` key; `{% if tools %}` on an UNDEFINED variable is false with no error, +// so the whole `# Tools` block was skipped and tool calling was dead, silently. Yokoten: +// Qwen2.5 and Qwen3 gate their tools block the same way, so all three are checked. +#[cfg(test)] +mod tools_reach_the_template_4650 { + use super::*; + + const QWEN25: &str = include_str!("fixtures/chat_template_3990/qwen25.jinja"); + const QWEN3: &str = include_str!("fixtures/chat_template_3990/qwen3.jinja"); + const QWEN35: &str = include_str!("fixtures/chat_template_3990/qwen35.jinja"); + + fn tools() -> serde_json::Value { + serde_json::json!([{ + "type": "function", + "function": { + "name": "get_weather", + "description": "Weather for a city, when a < b & c > d", + "parameters": { + "type": "object", + "properties": {"city": {"type": "string"}}, + "required": ["city"] + } + } + }]) + } + + fn render(tpl: &str, tools: Option<&serde_json::Value>) -> String { + let msgs = vec![ChatMessage::user("What is the weather in Paris?")]; + render_official_with_tools(tpl, None, None, &msgs, true, Some(false), tools) + .expect("renders") + } + + /// The tool, as `json.dumps(tool, ensure_ascii=False)` writes it (HuggingFace's `tojson`): + /// `", "` / `": "` separators and no HTML escaping. Keys are in serde_json's order. + const TOOL_JSON: &str = r#"{"function": {"description": "Weather for a city, when a < b & c > d", "name": "get_weather", "parameters": {"properties": {"city": {"type": "string"}}, "required": ["city"], "type": "object"}}, "type": "function"}"#; + + /// must-RED on the pre-#4650 renderer: the tools block is absent. + #[test] + fn every_qwen_template_renders_the_tools_block_4650() { + for (name, tpl) in [("qwen25", QWEN25), ("qwen3", QWEN3), ("qwen35", QWEN35)] { + let got = render(tpl, Some(&tools())); + let block = format!("\n{TOOL_JSON}\n"); + assert!( + got.contains(&block), + "{name}: no tools block {block:?} in:\n{got}" + ); + assert!( + got.contains(""), + "{name}: no call format in:\n{got}" + ); + assert!( + got.contains("What is the weather in Paris?"), + "{name}: the user turn is gone:\n{got}" + ); + } + } + + /// Qwen3.5 is told to call in XML -- the format the response parser must read. + #[test] + fn qwen35_instructs_the_xml_call_format_4650() { + let got = render(QWEN35, Some(&tools())); + assert!( + got.contains("\n"), + "{got}" + ); + } + + /// No tools is byte-identical to the renderer without the parameter: the #3990 cells + /// (which carry no tools) cannot move. + #[test] + fn no_tools_renders_exactly_what_render_official_renders_4650() { + let msgs = vec![ChatMessage::user("What is the weather in Paris?")]; + for tpl in [QWEN25, QWEN3, QWEN35] { + let plain = + render_official(tpl, None, None, &msgs, true, Some(false)).expect("renders"); + assert_eq!(render(tpl, None), plain); + assert!(!plain.contains("")); + } + } + + /// `tojson` is `json.dumps`: no `<` escapes, Python separators. minijinja's own + /// filter fails both. + #[test] + fn tojson_writes_what_json_dumps_writes_4650() { + let v = + minijinja::Value::from_serialize(serde_json::json!({"a": [1, "x'"], "b": null})); + let got = py_tojson(&v).expect("serializes"); + assert_eq!(got.as_str(), Some(r#"{"a": [1, "x'"], "b": null}"#)); + } +} diff --git a/crates/aprender-serve/src/grammar/tool.rs b/crates/aprender-serve/src/grammar/tool.rs index 4c9e69af87..fd9b3b226e 100644 --- a/crates/aprender-serve/src/grammar/tool.rs +++ b/crates/aprender-serve/src/grammar/tool.rs @@ -146,6 +146,9 @@ pub enum ToolCallFormat { Anthropic, /// Hermes format: {"name": "tool", "arguments": {...}} Hermes, + /// Qwen3.5 / Qwen3-Coder XML (#4650), the format the Qwen3.5 template instructs: + /// `VALUE` + QwenXml, } impl ToolCallParser { @@ -190,6 +193,7 @@ impl ToolCallParser { ToolCallFormat::OpenAI => self.parse_openai(text), ToolCallFormat::Anthropic => self.parse_anthropic(text), ToolCallFormat::Hermes => self.parse_hermes(text), + ToolCallFormat::QwenXml => self.parse_qwen_xml(text), } } @@ -285,6 +289,75 @@ impl ToolCallParser { calls } + + /// #4650: Qwen3.5's own call format. Each `...` block is one + /// call; the `` wrapper is not required, because models drop it. A call to a + /// tool the request did not declare is not a call, as in the other formats. + fn parse_qwen_xml(&mut self, text: &str) -> Vec { + const OPEN: &str = "') else { + break; + }; + let name = text[head..head + name_end].trim(); + let body_start = head + name_end + 1; + let body_end = text[body_start..] + .find("") + .map_or(text.len(), |e| body_start + e); + if let Some(tool) = self.tools.iter().find(|t| t.name == name) { + let arguments = qwen_xml_arguments(&text[body_start..body_end], tool); + calls.push(ToolCall::new(self.generate_id(), name, arguments.to_string())); + } + pos = body_end; + } + calls + } +} + +/// #4650: the `VALUE` pairs of one Qwen XML call, as a JSON +/// object. A missing `` ends at the next ` serde_json::Value { + const OPEN: &str = "') else { + break; + }; + let name = head[..name_end].trim(); + let after = &head[name_end + 1..]; + let end = [after.find(""), after.find(OPEN)] + .into_iter() + .flatten() + .min() + .unwrap_or(after.len()); + let value = qwen_xml_value(&after[..end], tool, name); + args.insert(name.to_string(), value); + rest = &after[end..]; + } + serde_json::Value::Object(args) +} + +/// #4650: a Qwen XML parameter value. The template puts the value on its own line, so one +/// newline either side is framing, not content. A parameter the tool declares as a string +/// stays a string verbatim (a zip code "02134" is not the number 2134); any other is read as +/// JSON when it parses as JSON, and kept as the string when it does not. +fn qwen_xml_value(raw: &str, tool: &ToolDefinition, name: &str) -> serde_json::Value { + let raw = raw.strip_prefix('\n').unwrap_or(raw); + let raw = raw.strip_suffix('\n').unwrap_or(raw); + let declared_string = tool + .parameters + .iter() + .find(|p| p.name == name) + .is_some_and(|p| matches!(p.param_type, ToolParameterType::String)); + if declared_string { + return serde_json::Value::String(raw.to_string()); + } + serde_json::from_str(raw.trim()).unwrap_or_else(|_| serde_json::Value::String(raw.to_string())) } /// Process a single character for brace matching state machine. diff --git a/docs/audits/surface_audit.csv b/docs/audits/surface_audit.csv index b552fc767f..3b533391a3 100644 --- a/docs/audits/surface_audit.csv +++ b/docs/audits/surface_audit.csv @@ -72,7 +72,7 @@ apr,POST /stream/generate,2,x86_64-linux,llama.cpp,yes,3,http-apr-serve,crates/a apr,POST /tokenize,2,x86_64-linux,llama.cpp,yes,3,http-apr-serve,crates/aprender-serve/src/api/router.rs:96,high apr,POST /v1/batch/completions,9,x86_64-linux,vllm,yes,3,http-apr-serve,crates/aprender-serve/src/api/router.rs:152,medium apr,POST /v1/chat/completions,4,x86_64-linux;nvidia-cuda,llama.cpp,yes,3,http-apr-serve,crates/aprender-serve/src/api/router.rs:134,high -apr,"POST /v1/chat/completions (stream:true, SSE)",4,x86_64-linux;nvidia-cuda,llama.cpp,no,3,http-apr-serve,"crates/aprender-serve/src/api/openai_handlers.rs:755-787 (stream branches crates/aprender-serve/src/api/cuda_chat_backend.rs:129,275,405,520,986; ignore_eos fail-closed 12,362,471)",high +apr,"POST /v1/chat/completions (stream:true, SSE)",4,x86_64-linux;nvidia-cuda,llama.cpp,no,3,http-apr-serve,"crates/aprender-serve/src/api/openai_handlers.rs:762-794 (stream branches crates/aprender-serve/src/api/cuda_chat_backend.rs:129,275,405,520,986; ignore_eos fail-closed 12,362,471)",high apr,POST /v1/chat/completions/stream,4,x86_64-linux,llama.cpp,yes,3,http-apr-serve,crates/aprender-serve/src/api/router.rs:139,high apr,POST /v1/completions,2,x86_64-linux,llama.cpp,yes,3,http-apr-serve,crates/aprender-serve/src/api/router.rs:131,high apr,"POST /v1/completions (stream:true, SSE)",2,x86_64-linux,llama.cpp,no,3,http-apr-serve,crates/aprender-serve/src/api/gpu_completions_handler.rs:405 (SSE :340-378),high diff --git a/docs/roadmaps/entries/PMAT-4650.yaml b/docs/roadmaps/entries/PMAT-4650.yaml new file mode 100644 index 0000000000..6f1cfe616f --- /dev/null +++ b/docs/roadmaps/entries/PMAT-4650.yaml @@ -0,0 +1,21 @@ +- id: PMAT-4650 + github_issue: 4650 + item_type: task + title: 'apr serve: pass request tools into the GGUF chat template and parse Qwen3.5 XML tool calls (tool calling was silently dead)' + status: inprogress + priority: critical + assigned_to: aprender-w4650 + created: 2026-10-04T00:00:00Z + updated: 2026-10-04T00:00:00Z + acceptance_criteria: + - With request `tools`, the Qwen2.5, Qwen3 and Qwen3.5 templates render their `` block, and the tool JSON is written as HF tojson writes it (json.dumps with Python separators and no HTML escaping) + - With no tools, the rendered prompt is byte-identical to before, so the 16 #3990 oracle cells do not move + - A Qwen3.5 XML call (`V`) to a declared tool returns tool_calls with schema-typed arguments and finish_reason "tool_calls". A call to an undeclared tool stays content + - The Qwen3.5, CUDA and dense chat paths all pass request.tools to the renderer + - 'Out of scope, follow-ups: streaming tool_calls; replaying assistant tool_calls and tool-role messages into the template; tools on the legacy template path (it warns)' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:bug + notes: 'Filed by alfredodeza. Operator: must hit 0.70.2.' diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 8dd540de02..02b7368345 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -24075,6 +24075,27 @@ Not done, pending the ruling: moving the 31 fixture files to an obviously synthe labels: - kind:code notes: 'Operator C187(a)/C188(a), 2026-10-01: ratchet, not a waiver.' +- id: PMAT-4650 + github_issue: 4650 + item_type: task + title: 'apr serve: pass request tools into the GGUF chat template and parse Qwen3.5 XML tool calls (tool calling was silently dead)' + status: inprogress + priority: critical + assigned_to: aprender-w4650 + created: 2026-10-04T00:00:00Z + updated: 2026-10-04T00:00:00Z + acceptance_criteria: + - With request `tools`, the Qwen2.5, Qwen3 and Qwen3.5 templates render their `` block, and the tool JSON is written as HF tojson writes it (json.dumps with Python separators and no HTML escaping) + - With no tools, the rendered prompt is byte-identical to before, so the 16 #3990 oracle cells do not move + - A Qwen3.5 XML call (`V`) to a declared tool returns tool_calls with schema-typed arguments and finish_reason "tool_calls". A call to an undeclared tool stays content + - The Qwen3.5, CUDA and dense chat paths all pass request.tools to the renderer + - 'Out of scope, follow-ups: streaming tool_calls; replaying assistant tool_calls and tool-role messages into the template; tools on the legacy template path (it warns)' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:bug + notes: 'Filed by alfredodeza. Operator: must hit 0.70.2.' - id: PMAT-4651 github_issue: 4651 item_type: task diff --git a/evidence/pr-review/4711/0b6096d7523b3ca22872f5fe0d94c956c78c05ca/agy.json b/evidence/pr-review/4711/0b6096d7523b3ca22872f5fe0d94c956c78c05ca/agy.json new file mode 100644 index 0000000000..182f3174c8 --- /dev/null +++ b/evidence/pr-review/4711/0b6096d7523b3ca22872f5fe0d94c956c78c05ca/agy.json @@ -0,0 +1 @@ +{"conversation_id":"d2f5d6f9-dbec-4c9b-9a98-549ff5b8a74b","status":"ERROR","response":"{\"findings\":[{\"failure_scenario\":\"The fit check incorrectly assumes the batched prefill cache fits, but the continuous-batching scheduler later attempts to allocate the extra SERVE_BATCH_SLOTS slots, leading to an OOM or a degraded allocation.\",\"file\":\"crates/aprender-serve/src/gguf/cuda/mod.rs\",\"grounding\":\"asserted\",\"id\":\"fit_serving_need_mis_sizing\",\"line\":837,\"summary\":\"The need byte calculation for the FP16 batched prefill fit check does not account for SERVE_BATCH_SLOTS when KvLen::FitServing is used.\"},{\"failure_scenario\":\"The Serial path does not require large score buffers (e.g., 1 GiB), but this VRAM remains artificially subtracted from the room calculation. The session is incorrectly limited in context length, causing long chat turns to time out or fall back to the CPU.\",\"file\":\"crates/aprender-serve/src/gguf/cuda/mod.rs\",\"grounding\":\"asserted\",\"id\":\"unrecalculated_session_kv_len\",\"line\":814,\"summary\":\"After the fit check switches to Serial, max_seq_len is not recomputed to reclaim the VRAM reserved for the batched-prefill score buffers.\"},{\"failure_scenario\":\"If cudaMemGetInfo fails, the remaining logic sizes the KV to the 2048 floor and forces Serial prefill. The chat session becomes slow and short, and the log incorrectly blames VRAM scarcity.\",\"file\":\"crates/aprender-serve/src/gguf/cuda/mod.rs\",\"grounding\":\"asserted\",\"id\":\"memory_probe_failure_reads_as_zero_free\",\"line\":798,\"summary\":\"A failed cudaMemGetInfo probe defaults to 0 free VRAM, silently forcing Serial prefill for QK-norm models.\"},{\"failure_scenario\":\"If the model emits multiple \\u003cfunction=...\\u003e calls but forgets the closing \\u003c/function\\u003e tag, body_end extends to the end of the text. The remaining distinct function calls are swallowed as arguments of the first function.\",\"file\":\"crates/aprender-serve/src/grammar/tool.rs\",\"grounding\":\"asserted\",\"id\":\"qwen_xml_function_parsing_fails_on_missing_close_tag\",\"line\":301,\"summary\":\"In parse_qwen_xml, body_end only looks for \\u003c/function\\u003e and does not fall back to checking for the next \\u003cfunction= tag.\"},{\"failure_scenario\":\"For models with large vocabularies, token embeddings take significant VRAM (e.g., ~1GB for Qwen3). This omission causes r.weights to severely underestimate the model's footprint, potentially causing the fit check to admit a batched prefill cache that will OOM during allocation.\",\"file\":\"crates/aprender-serve/src/gguf/cuda/mod.rs\",\"grounding\":\"asserted\",\"id\":\"resident_estimate_underestimates_weights\",\"line\":963,\"summary\":\"resident_estimate calculates r.weights by summing only the projection tensors and LM head, omitting token embeddings and other weights.\"}],\"reviewed\":true,\"toolAction\":\"Finishing review\",\"toolSummary\":\"Complete review\"}\n","error":"API error (attempt 1): UNAVAILABLE (code 503): The service is currently unavailable.","duration_seconds":148.484594825,"num_turns":1,"structured_output":{"findings":[{"failure_scenario":"The fit check incorrectly assumes the batched prefill cache fits, but the continuous-batching scheduler later attempts to allocate the extra SERVE_BATCH_SLOTS slots, leading to an OOM or a degraded allocation.","file":"crates/aprender-serve/src/gguf/cuda/mod.rs","grounding":"asserted","id":"fit_serving_need_mis_sizing","line":837,"summary":"The need byte calculation for the FP16 batched prefill fit check does not account for SERVE_BATCH_SLOTS when KvLen::FitServing is used."},{"failure_scenario":"The Serial path does not require large score buffers (e.g., 1 GiB), but this VRAM remains artificially subtracted from the room calculation. The session is incorrectly limited in context length, causing long chat turns to time out or fall back to the CPU.","file":"crates/aprender-serve/src/gguf/cuda/mod.rs","grounding":"asserted","id":"unrecalculated_session_kv_len","line":814,"summary":"After the fit check switches to Serial, max_seq_len is not recomputed to reclaim the VRAM reserved for the batched-prefill score buffers."},{"failure_scenario":"If cudaMemGetInfo fails, the remaining logic sizes the KV to the 2048 floor and forces Serial prefill. The chat session becomes slow and short, and the log incorrectly blames VRAM scarcity.","file":"crates/aprender-serve/src/gguf/cuda/mod.rs","grounding":"asserted","id":"memory_probe_failure_reads_as_zero_free","line":798,"summary":"A failed cudaMemGetInfo probe defaults to 0 free VRAM, silently forcing Serial prefill for QK-norm models."},{"failure_scenario":"If the model emits multiple \u003cfunction=...\u003e calls but forgets the closing \u003c/function\u003e tag, body_end extends to the end of the text. The remaining distinct function calls are swallowed as arguments of the first function.","file":"crates/aprender-serve/src/grammar/tool.rs","grounding":"asserted","id":"qwen_xml_function_parsing_fails_on_missing_close_tag","line":301,"summary":"In parse_qwen_xml, body_end only looks for \u003c/function\u003e and does not fall back to checking for the next \u003cfunction= tag."},{"failure_scenario":"For models with large vocabularies, token embeddings take significant VRAM (e.g., ~1GB for Qwen3). This omission causes r.weights to severely underestimate the model's footprint, potentially causing the fit check to admit a batched prefill cache that will OOM during allocation.","file":"crates/aprender-serve/src/gguf/cuda/mod.rs","grounding":"asserted","id":"resident_estimate_underestimates_weights","line":963,"summary":"resident_estimate calculates r.weights by summing only the projection tensors and LM head, omitting token embeddings and other weights."}],"reviewed":true},"json_schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://paiml.dev/schemas/pr-review/agy-review-v1.json","title":"PR-REVIEW-SKILL-002 v2 §3.E — the structured output agy is asked for","$comment":"NOT under schemas/. That directory is VENDORED UPSTREAM artifacts only — two files, a SHA-256 manifest and a sources.json recording where each came from, enforced by scripts/check_vendored_schemas.sh and its own mutation guard. This schema is authored here, has no upstream, and would have no honest row in sources.json; putting it there would make a provenance record that records nothing. It is the skill's own output contract, so it lives with the skill.","type":"object","additionalProperties":false,"required":["reviewed","findings"],"properties":{"reviewed":{"type":"boolean","description":"false means agy declined or could not review. The caller records status: unreachable and verdict DEGRADED — never a run that found nothing."},"findings":{"type":"array","description":"May be empty. An empty array under reviewed:true is 'ran and found nothing', which is a different artifact from 'could not run' (§3.0).","items":{"type":"object","additionalProperties":false,"required":["id","grounding","summary","failure_scenario"],"properties":{"id":{"type":"string","minLength":1},"grounding":{"enum":["cited","measured","asserted"],"description":"§1's three marks and there is no fourth. A `measured` finding here was measured by AGY's process, not the primary reviewer's — the receipt records that in reverified_by_primary (§3.E.5)."},"summary":{"type":"string","minLength":1},"failure_scenario":{"type":"string","minLength":1,"description":"§4.2: a finding that cannot name the concrete failure it permits is a comment, not a finding."},"file":{"type":"string"},"line":{"type":"integer","minimum":1},"source":{"type":"string","description":"Required by the guard when grounding is `cited`: a citation with no source is an assertion wearing the mark of a citation (§1.1)."},"command":{"type":"array","items":{"type":"string"},"description":"argv, when grounding is `measured`. It records what AGY ran, and the primary reviewer does not re-run it (§3.E.5)."}}}}}},"usage":{"input_tokens":100498,"output_tokens":21208,"thinking_tokens":19986,"cache_read_tokens":224161,"total_tokens":121706}} diff --git a/evidence/pr-review/4711/0b6096d7523b3ca22872f5fe0d94c956c78c05ca/findings.sarif b/evidence/pr-review/4711/0b6096d7523b3ca22872f5fe0d94c956c78c05ca/findings.sarif new file mode 100644 index 0000000000..79f07b7b9b --- /dev/null +++ b/evidence/pr-review/4711/0b6096d7523b3ca22872f5fe0d94c956c78c05ca/findings.sarif @@ -0,0 +1,357 @@ +{ + "version": "2.1.0", + "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json", + "runs": [ + { + "tool": { + "driver": { + "name": "pmat" + } + }, + "results": [ + { + "ruleId": "complexity_delta", + "level": "note", + "message": { + "text": "pmat analyze complexity over head and base copies of the 17 changed non-test .rs files. Only five functions rise: build_tool_calling_message 3->5 (re-measured at f70cf5a8fa), to_grammar 1->3, extract_tool_parameters 3->5, gguf/cuda/mod.rs build 6->8 (from #4623), serve run 6->7; all under 10. The extract-fn refactor lowers try_cuda_backend from cyclomatic 14 / cognitive 43 to 9 / 20 and completions_inner from 9 / 26 to 8 / 21. At 0b6096d752 build_tool_calling_message measures 4/7 (cyclomatic/cognitive) vs base 3/6; the previous head was 5/8, so the fallback removal lowers it." + }, + "properties": { + "grounding": "measured", + "command": [ + "pmat", + "analyze", + "complexity", + "--format", + "json", + "--path", + "" + ], + "exit_code": 0, + "stdout_sha256": "d811c87edb1a71e8ad5a404cb618a58cae0cb7b704af4be14f5efed8f5f92017", + "failure_scenario": "a later branch in extract_tool_parameters or try_cuda_backend crosses cyclomatic 10", + "precision_class": "advisory" + } + }, + { + "ruleId": "tdg_delta", + "level": "note", + "message": { + "text": "pmat analyze tdg: no changed file drops a grade. handler_gpu_completion.rs 89.79 -> 88.52, still A-. grammar/tool.rs and gguf/cuda/mod.rs have no base copy in the 17-file set (A at head)." + }, + "properties": { + "grounding": "measured", + "command": [ + "pmat", + "analyze", + "tdg", + "--format", + "json", + "--path", + "" + ], + "exit_code": 0, + "stdout_sha256": "e22114fee548d3aaf38120f9fc90d606f151177cd5e987d43c616a0e0f671442", + "failure_scenario": "none: grade unchanged", + "precision_class": "advisory" + } + }, + { + "ruleId": "satd_none_introduced", + "level": "note", + "message": { + "text": "pmat analyze satd over the 17 head files and the base copies: 0 violations on both." + }, + "properties": { + "grounding": "measured", + "command": [ + "pmat", + "analyze", + "satd", + "--format", + "json", + "--path", + "" + ], + "exit_code": 0, + "stdout_sha256": "4c677a9e5f190ffaa95b108f941bf34ba9f30a27c1aea16cbfbad1ac4f325e28", + "failure_scenario": "a TODO added by this diff would be counted", + "precision_class": "advisory" + } + }, + { + "ruleId": "refactor_equivalence_try_cuda_backend", + "level": "note", + "message": { + "text": "Commit 26ccc7f932 extracts dispatch_cuda_stream, cuda_batch_collect, cuda_direct_collect and try_cuda_direct_completions. Read line by line: the 503 batch-queue-full path keeps record_admission_rejected, the 500 and generation_error_status paths are unchanged, timing_tx sends are unchanged, and chain order is unchanged. Two clones became moves. No behaviour change found." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/aprender-serve/src/api/cuda_chat_backend.rs" + }, + "region": { + "startLine": 1 + } + } + } + ], + "properties": { + "grounding": "asserted", + "rationale": "Read by the primary against the base copy; no cargo test could be run on this host, and the extracted paths are device paths.", + "failure_scenario": "a dropped record_admission_rejected would under-count 503s with no test failing", + "precision_class": "advisory" + } + }, + { + "ruleId": "qwen_xml_parser_edge_cases", + "level": "note", + "message": { + "text": "parse_qwen_xml finds or at end of text, and does not skip think blocks. A call quoted inside reasoning, a truncated final call, or a literal inside a string value parse as a call or cut the value short." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/aprender-serve/src/grammar/tool.rs" + }, + "region": { + "startLine": 296 + } + } + } + ], + "properties": { + "grounding": "asserted", + "rationale": "Read of parse_qwen_xml, qwen_xml_arguments and qwen_xml_value. The module is include!d so no mutation or unit run covers it from this host.", + "failure_scenario": "a value containing is truncated at that tag and the rest is dropped", + "precision_class": "advisory" + } + }, + { + "ruleId": "legacy_template_tools_warning_noise", + "level": "note", + "message": { + "text": "The legacy template path prints an eprintln! warning on every request that carries tools. Honest, but it is stderr noise per request and not a tracing event." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/aprender-serve/src/api/realize_handlers.rs" + }, + "region": { + "startLine": 287 + } + } + } + ], + "properties": { + "grounding": "asserted", + "rationale": "Read; not run.", + "failure_scenario": "a client sending tools on every turn fills the server log", + "precision_class": "advisory" + } + }, + { + "ruleId": "qwen_xml_final_no_fallback", + "level": "note", + "message": { + "text": "0b6096d752 makes the QwenXml result FINAL when the text contains block outranks a later JSON call. Not run here (no cargo on this host); the RED-at-f70, GREEN-at-head gx10 result is the author claim." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/aprender-serve/src/api/openai_handlers.rs" + }, + "region": { + "startLine": 643 + } + } + } + ], + "properties": { + "grounding": "asserted", + "rationale": "Read git diff f70cf5a8fa..0b6096d752 (one file) and lines 640-660; no test executed by the reviewer.", + "failure_scenario": "model text that mentions --jobs 1 --cargo-arg=--lib, test set filtered to 4650 pmat801 chat_template_wiring thinking_on_refusal chat_template_official format_chat. 18 mutants, 11 caught, 7 missed, 0 unviable, 0 timeout. grammar/tool.rs, chat_template_official.rs and mod_create_demo.rs are include!d and cargo-mutants did not enumerate them, so the parser and the tojson filter went unmutated." + } + } + ] + } + ], + "results": [ + { + "ruleId": "mutants_survivors_in_diff", + "level": "warning", + "message": { + "text": "7 of 18 in-diff mutants survived: try_gpu_backend -> None and Some(Default::default()) (openai_handlers.rs:1067), try_cached_backend -> None and Some(Default::default()) (openai_handlers.rs:1185), delete ! in format_chat_messages_official_thinking_tools (realize_handlers.rs:275), and format_chat_messages_for_state_thinking -> Ok(String::new()) and Ok(\"xyzzy\") (realize_handlers.rs:323). The test filter limited which tests could kill them, so some may be killed by the full lib suite. The realize_handlers.rs:275 survivor is the empty-tools guard of the #4650 fix." + }, + "properties": { + "grounding": "measured", + "command": [ + "cargo", + "mutants", + "-p", + "aprender-serve", + "--in-diff", + "", + "--jobs", + "1", + "--cargo-arg=--lib" + ], + "exit_code": 2, + "stdout_sha256": "4580a915a8c1438873474bf135d4f078c0c79913aff45be31cf2b23ba076ee35", + "failure_scenario": "deleting the ! at realize_handlers.rs:275 makes the filter keep empty tool lists and render an empty tools block, and no filtered test fails", + "precision_class": "advisory" + } + } + ] + }, + { + "tool": { + "driver": { + "name": "antigravity" + } + }, + "results": [ + { + "ruleId": "agy_fit_serving_need_single_slot", + "level": "note", + "message": { + "text": "antigravity: with KvLen::FitServing the fit check computes need for one KV slot while serving provisions 1 + SERVE_BATCH_SLOTS. Not re-verified by the primary." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/aprender-serve/src/gguf/cuda/mod.rs" + }, + "region": { + "startLine": 837 + } + } + } + ], + "properties": { + "grounding": "asserted", + "rationale": "Raised by antigravity; the primary read line 837 but did not trace the batch-slot allocation.", + "failure_scenario": "the FP16 prefill cache is kept when the batch slots then no longer fit and the scheduler OOMs at allocation", + "precision_class": "advisory" + } + }, + { + "ruleId": "agy_session_kv_len_not_recomputed_after_serial", + "level": "note", + "message": { + "text": "antigravity: after the switch to serial prefill max_seq_len stays clamped by the batched-prefill reserve. Agy cites line 838; the primary saw max_seq_len fixed at 829-831. Not re-verified." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/aprender-serve/src/gguf/cuda/mod.rs" + }, + "region": { + "startLine": 829 + } + } + } + ], + "properties": { + "grounding": "asserted", + "rationale": "Raised by antigravity; the primary read the ordering and did not measure the effect.", + "failure_scenario": "Qwen3-8B on 24 GB serves a smaller context than the memory allows", + "precision_class": "advisory" + } + }, + { + "ruleId": "agy_memory_probe_failure_reads_zero_free", + "level": "note", + "message": { + "text": "antigravity: executor.memory_info().unwrap_or((0, 0)) reads a failed cudaMemGetInfo as 0 bytes free and silently forces serial prefill; the eprintln then prints the 0.0 GB figure." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/aprender-serve/src/gguf/cuda/mod.rs" + }, + "region": { + "startLine": 820 + } + } + } + ], + "properties": { + "grounding": "asserted", + "rationale": "Raised by antigravity; the primary read 820-847 and confirms the unwrap_or((0,0)) and the eprintln.", + "failure_scenario": "a transient driver error at load leaves a 2048 context and serial prefill with only a stderr line", + "precision_class": "advisory" + } + } + ] + } + ] +} diff --git a/evidence/pr-review/4711/0b6096d7523b3ca22872f5fe0d94c956c78c05ca/receipt.intoto.jsonl b/evidence/pr-review/4711/0b6096d7523b3ca22872f5fe0d94c956c78c05ca/receipt.intoto.jsonl new file mode 100644 index 0000000000..157425d083 --- /dev/null +++ b/evidence/pr-review/4711/0b6096d7523b3ca22872f5fe0d94c956c78c05ca/receipt.intoto.jsonl @@ -0,0 +1 @@ +{"_type":"https://in-toto.io/Statement/v1","subject":[{"name":"git+https://github.com/paiml/aprender","digest":{"sha1":"0b6096d7523b3ca22872f5fe0d94c956c78c05ca"}}],"predicateType":"https://paiml.dev/attestations/pr-review/v2","predicate":{"skill_version":"2.1.0","attestation_level":"L1-self","pr":4711,"base_sha":"11f844a772f77647d7d4b6281acbb2ab5e0bd76b","head_sha":"0b6096d7523b3ca22872f5fe0d94c956c78c05ca","author_actor":{"kind":"agent","id":"agent:claude-opus-5-5/aprender-w4711"},"reviewer_actor":{"kind":"agent","id":"agent:claude-sonnet-5-5/w4711-review"},"affected_crates":["aprender-serve","apr-cli"],"verdict":"DEGRADED","degraded_reason":"mutation ran on gx10 at 942e3f8fe6 only with a filtered test set and left 7 of 18 in-diff mutants surviving; it was NOT re-run for 0b6096d752, which makes the QwenXml result final (drops the JSON fallback) of build_tool_calling_message in openai_handlers.rs (new code unmutated); the include!d files grammar/tool.rs, chat_template_official.rs and mod_create_demo.rs are invisible to cargo-mutants; no cargo test or device run was possible on this host, so the new qwen35_undeclared_xml_call_does_not_promote_inner_json_4650 test (RED at f70cf5a8fa, GREEN at head per gx10 author claim) and the K1 gx10 results rest on the author logs and reading; the cuda-docs-reviewer agent could not be dispatched from this lane so nvidia-cuda-docs was queried inline once at 68b281950c (no authority found) and not re-queried; origin/main could not be refreshed (fetch blocked), local origin/main equals base; the agy tree was built from git ls-files of a clean worktree because the lane hook refuses git archive; agy status was ERROR (503 on attempt 1) yet it returned a reviewed schema-valid structured_output, and its 5 findings were not all re-verified; it did not review the parser change; affected_crates derived from file paths. No blocking class (B1-B6) fires.","consultations":{"pmat":{"status":"consulted","transport":"cli","transport_unavailable":["mcp: not attempted this run (cli answered)"],"index_commit":"0b6096d7523b3ca22872f5fe0d94c956c78c05ca","index_is_ancestor":true,"index_worktree_dirty":false,"complexity_delta":[{"file":"crates/aprender-serve/src/api/openai_handlers.rs","function":"build_tool_calling_message","base_cyclomatic":3,"head_cyclomatic":4,"base_cognitive":6,"head_cognitive":7},{"file":"crates/aprender-serve/src/gguf/cuda/mod.rs","function":"build","base_cyclomatic":6,"head_cyclomatic":8,"base_cognitive":11,"head_cognitive":13},{"file":"crates/apr-cli/src/commands/serve/mod.rs","function":"run","base_cyclomatic":6,"head_cyclomatic":7,"base_cognitive":7,"head_cognitive":8},{"file":"crates/aprender-serve/src/api/mod_create_demo.rs","function":"to_grammar","base_cyclomatic":1,"head_cyclomatic":3,"base_cognitive":0,"head_cognitive":5},{"file":"crates/aprender-serve/src/api/mod_create_demo.rs","function":"extract_tool_parameters","base_cyclomatic":3,"head_cyclomatic":5,"base_cognitive":2,"head_cognitive":6}],"tdg_delta":[{"file":"crates/apr-cli/src/commands/serve/handler_gpu_completion.rs","base":"A-","head":"A-","base_total":89.79,"head_total":88.52},{"file":"crates/aprender-serve/src/grammar/tool.rs","base":null,"head":"A","base_total":null,"head_total":91.0},{"file":"crates/aprender-serve/src/gguf/cuda/mod.rs","base":null,"head":"A","base_total":null,"head_total":90.0}],"satd_introduced":[],"cache_hits":0,"analysis_note":"pmat analyze complexity, satd and tdg over head and base copies of the 17 changed non-test .rs files. 0 SATD on both. Complexity of try_cuda_backend falls 14/43 -> 9/20 and completions_inner 9/26 -> 8/21. Scores are per copy and context dependent; files without a base copy show base null. Reused from 942e3f8fe6: the only later code commit (68b281950c) changes one test file, a cfg-test fn signature and an attribute, no new branch. At e95438e370 the K1 test is no longer #[ignore] (the module is cfg(feature=cuda), so cuda-unit runs it by name and the earlier lane-coverage finding is resolved); it still panics on init failure. At f70cf5a8fa build_tool_calling_message was re-measured with pmat analyze complexity on base and head copies: cyclomatic 3->5, cognitive 6->8. Other deltas reused from 942e3f8fe6. At 0b6096d752 only openai_handlers.rs changed (build_tool_calling_message re-measured 4/7 vs base 3/6, from head and base copies); other measurements reused from f70cf5a8fa.","duplication_hits":[{"needle":"args_of","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-contracts/src/ontology/extract/cli_surface.rs","line":209,"method":"lexical"},{"needle":"args_of","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-contracts/src/ontology/extract/cli_surface.rs","line":217,"method":"lexical"},{"needle":"args_of","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-contracts/src/ontology/extract/cli_surface.rs","line":232,"method":"lexical"},{"needle":"fp16_cache","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-serve/src/gpu/streaming_kv_streaming.rs","line":278,"method":"lexical"},{"needle":"fp16_cache","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-serve/src/gpu/streaming_kv_streaming.rs","line":281,"method":"lexical"},{"needle":"fp16_cache","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-serve/tests/gpu_coverage.rs","line":1552,"method":"lexical"},{"needle":"fp16_cache","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-serve/tests/gpu_coverage.rs","line":1554,"method":"lexical"},{"needle":"resolve_serve_max_seq_len","kind":"name","where":"HEAD","ref":"HEAD","path":"evidence/serve/3791/Q4K.md","line":23,"method":"lexical"},{"needle":"serve_kv_limit_k1.rs","kind":"filename","where":"branch","ref":"origin/devont/k1-revert-red","path":"crates/aprender-serve/src/api/tests/serve_kv_limit_k1.rs","line":0,"method":"lexical"},{"needle":"session_kv.rs","kind":"filename","where":"branch","ref":"origin/devont/k1-revert-red","path":"crates/aprender-serve/src/gguf/cuda/session_kv.rs","line":0,"method":"lexical"},{"needle":"session_kv.rs","kind":"filename","where":"branch","ref":"origin/ec/3715-chunked-prefill","path":"crates/aprender-serve/src/gguf/cuda/session_kv.rs","line":0,"method":"lexical"},{"needle":"serve_kv_limit_k1.rs","kind":"filename","where":"branch","ref":"origin/pr/4631","path":"crates/aprender-serve/src/api/tests/serve_kv_limit_k1.rs","line":0,"method":"lexical"},{"needle":"session_kv.rs","kind":"filename","where":"branch","ref":"origin/pr/4631","path":"crates/aprender-serve/src/gguf/cuda/session_kv.rs","line":0,"method":"lexical"},{"needle":"serve_kv_limit_k1.rs","kind":"filename","where":"branch","ref":"origin/verify/6a-4606","path":"crates/aprender-serve/src/api/tests/serve_kv_limit_k1.rs","line":0,"method":"lexical"},{"needle":"session_kv.rs","kind":"filename","where":"branch","ref":"origin/verify/6a-4606","path":"crates/aprender-serve/src/gguf/cuda/session_kv.rs","line":0,"method":"lexical"},{"needle":"PMAT-4636.yaml","kind":"filename","where":"branch","ref":"origin/verify/6a-4606","path":"docs/roadmaps/entries/PMAT-4636.yaml","line":0,"method":"lexical"}],"duplication_coverage":{"rust":"semantic","shell":"lexical","python":"lexical","config":"lexical","docs":"lexical","other":"lexical","sibling_branches":"lexical","merge_base_to_main":"lexical"},"duplication_horizon":["head=0b6096d7523b3ca22872f5fe0d94c956c78c05ca","siblings=refs/remotes/origin/* unmerged into origin/main","merge_base_to_main=11f844a772f77647d7d4b6281acbb2ab5e0bd76b..refs/remotes/origin/main"],"horizon_branches_total":1406,"horizon_branches_scanned":1406,"merge_base_to_main_files":0,"symbols_searched":71,"queries":[{"q":"qwen xml tool call parser function parameter","command":"pmat query \"qwen xml tool call parser function parameter\" --limit 3","cwd":"worktree at HEAD","results":3,"note":"top hits are parse_qwen_xml and qwen_xml_arguments in this PR; no prior implementation surfaced"}],"duplication_note":"all 16 hits are the K1/session-KV files from the merged #4623 on sibling branches, generic fp16_cache and args_of names, or an evidence page; none is landed prior art for the tools fix (merge_base_to_main_files 0)."},"cuda":{"status":"consulted","trigger_reason":"S3.B path trigger fires on api/cuda_chat_backend.rs and the #4623 files under gguf/cuda/ (path contains cuda). The diff adds no device-behaviour claim: the extract-fn refactor moves host code, and #4623 carries its own queries. The cuda-docs-reviewer agent could not be dispatched from this lane, so the primary queried the nvidia-cuda-docs MCP inline once. Re-fired at 68b281950c: serve_kv_limit_k1.rs exercises OwnedQuantizedModelCuda. Re-fired at e95438e370 (same test file). Not re-queried at f70cf5a8fa: the new commit is host-side parser code in openai_handlers.rs.","queries":[{"q":"CUDA context bound to calling thread when running GPU kernels from a spawned blocking thread; cuCtxSetCurrent","source_uri":"nvidia-cuda-docs MCP (search_cuda_docs)","result":"no-authority-found","claim_checked":"The refactor keeps dispatch_cuda_stream inside the same blocking task as before, so the thread-current CUDA context is unchanged. The query returned the threadMigration driver-API sample (cuCtxPushCurrent per host thread) and nothing on spawn_blocking, so no authority was found for or against the claim."},{"q":"cuInit / device count zero: CUDA_ERROR_NO_DEVICE handling when no CUDA device is present","source_uri":"nvidia-cuda-docs MCP (search_cuda_docs)","result":"no-authority-found","claim_checked":"68b281950c panics when OwnedQuantizedModelCuda::for_serving fails to initialise a device. The query returned the cudaDeviceInit sample (cuInit, cuDeviceGetCount, exit on zero devices) which shows zero devices is an error path in the driver API, but nothing on how the Rust wrapper surfaces it, so no authority was found for or against the panic path."}]},"crux":{"status":"consulted","surfaces":["POST /v1/chat/completions request field tools","POST /v1/chat/completions response message.tool_calls and finish_reason"],"contracts":["contracts/crux-C-11-v1.yaml","contracts/crux-C-03-v1.yaml"],"gap_effect":"none","crux_coverage":"covered","comparative_claims":[]},"mutation":{"status":"consulted","scope":"in-diff","attempted":18,"killed":11,"survivors":[{"mutant":"replace try_gpu_backend -> Option with None","file":"crates/aprender-serve/src/api/openai_handlers.rs","line":1067,"killed":false},{"mutant":"replace try_gpu_backend -> Option with Some(Default::default())","file":"crates/aprender-serve/src/api/openai_handlers.rs","line":1067,"killed":false},{"mutant":"replace try_cached_backend -> Option with None","file":"crates/aprender-serve/src/api/openai_handlers.rs","line":1185,"killed":false},{"mutant":"replace try_cached_backend -> Option with Some(Default::default())","file":"crates/aprender-serve/src/api/openai_handlers.rs","line":1185,"killed":false},{"mutant":"delete ! in format_chat_messages_official_thinking_tools","file":"crates/aprender-serve/src/api/realize_handlers.rs","line":275,"killed":false},{"mutant":"replace format_chat_messages_for_state_thinking with Ok(String::new())","file":"crates/aprender-serve/src/api/realize_handlers.rs","line":323,"killed":false},{"mutant":"replace format_chat_messages_for_state_thinking with Ok(\"xyzzy\".into())","file":"crates/aprender-serve/src/api/realize_handlers.rs","line":323,"killed":false}]},"antigravity":{"status":"consulted","attempted":1,"agy_version":"agy 1.2.17","binary_path":"/home/noah/.local/bin/agy","model_id":"gemini-3.1-pro-high","model_family":"google/gemini","exit_code":0,"duration_seconds":148.484594825,"agy_status":"ERROR","usage":{"input_tokens":100498,"output_tokens":21208,"thinking_tokens":19986,"cache_read_tokens":224161,"total_tokens":121706},"output_check":{"structured_output_present":true,"reviewed":true,"schema_valid":true},"schema_check":"check-jsonschema --schemafile .claude/skills/pr-review/agy-review-v1.schema.json : ok","reverified_by_primary":false,"divergence_note":"agy status ERROR (API 503 on attempt 1) yet exit 0 and a reviewed, schema-valid structured_output, so recorded consulted with the status as a diagnostic. Disposable tree from git ls-files of the clean worktree at 0b6096d752, diff = merge-base..head excluding evidence/pr-review/4711. agy raised 5 asserted findings (4 in gguf/cuda/mod.rs #4623 code, 1 in grammar/tool.rs parse_qwen_xml missing close tag); primary read the cited cuda lines only and did not re-verify the rest. agy did not address the parser-final change.","divergence":{"agreed":0,"agy_only":5,"primary_only":9,"contradicted":0},"findings":[{"id":"fit_serving_need_mis_sizing","file":"crates/aprender-serve/src/gguf/cuda/mod.rs","grounding":"asserted"},{"id":"unrecalculated_session_kv_len","file":"crates/aprender-serve/src/gguf/cuda/mod.rs","grounding":"asserted"},{"id":"memory_probe_failure_reads_as_zero_free","file":"crates/aprender-serve/src/gguf/cuda/mod.rs","grounding":"asserted"},{"id":"qwen_xml_function_parsing_fails_on_missing_close_tag","file":"crates/aprender-serve/src/grammar/tool.rs","grounding":"asserted"},{"id":"resident_estimate_underestimates_weights","file":"crates/aprender-serve/src/gguf/cuda/mod.rs","grounding":"asserted"}]}},"findings_ref":{"path":"findings.sarif","sha256":"51a9099a4f2dd7feae1b3840af5c261b0c40775e7762e721847e225c8937239b"},"cost":{"input_tokens":337713,"output_tokens":42491,"wall_seconds":3600},"diff_patch_id":"92420ee17a4ccc09d952f877c3578a8ce5cf35d4","diff_patch_id_algo":"git-patch-id-verbatim/pinned-diff-v1"}} diff --git a/evidence/pr-review/4711/25f354a2063014cc451a315cc3be30d3ed8ade3a/findings.sarif b/evidence/pr-review/4711/25f354a2063014cc451a315cc3be30d3ed8ade3a/findings.sarif new file mode 100644 index 0000000000..9e78f52096 --- /dev/null +++ b/evidence/pr-review/4711/25f354a2063014cc451a315cc3be30d3ed8ade3a/findings.sarif @@ -0,0 +1,286 @@ +{ + "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "pmat" + } + }, + "results": [ + { + "ruleId": "complexity_delta", + "level": "note", + "message": { + "text": "pmat analyze complexity over head and base copies of the 9 changed non-test .rs files under crates/. Three functions rise, all under 10: build_tool_calling_message 3->4 (cognitive 6->7), to_grammar 1->3 (0->5), extract_tool_parameters 3->5 (2->6). No function from the removed #4623/#4630 files appears: gguf/cuda/mod.rs build and serve run are now identical to base. Values equal the f2338256ee receipt for the same files." + }, + "properties": { + "grounding": "measured", + "command": [ + "pmat", + "analyze", + "complexity", + "--format", + "json", + "--path", + "" + ], + "exit_code": 0, + "stdout_sha256": "e4593818370d567fb10e89d39e4ca32da51d6289937a486ee0326bd6dd2a0431", + "failure_scenario": "a later branch in extract_tool_parameters crosses cyclomatic 10", + "precision_class": "advisory" + } + }, + { + "ruleId": "tdg_delta", + "level": "note", + "message": { + "text": "pmat analyze tdg over head and base copies. No file changes letter grade except chat_template_official.rs A -> A+ (94.5 -> 98.2). cuda_chat_backend.rs B- 71.12 -> 71.02, openai_handlers.rs A- 87.52 -> 86.55, grammar/tool.rs A 93.5 -> 91.0, gpu_completions_handler.rs A 91.2 -> 90.3, realize_handlers.rs A 91.48 -> 91.02." + }, + "properties": { + "grounding": "measured", + "command": [ + "pmat", + "analyze", + "tdg", + "--format", + "json", + "--path", + "" + ], + "exit_code": 0, + "stdout_sha256": "4b91bd0fa5971cd6117b79314274ba502ed4255e8ead905eac32a25c3658ed8b", + "failure_scenario": "a file crosses below grade B and the tdg gate flips", + "precision_class": "advisory" + } + }, + { + "ruleId": "satd_none_introduced", + "level": "note", + "message": { + "text": "pmat analyze satd reports 0 violations on both the head copy (9 files) and the base copy." + }, + "properties": { + "grounding": "measured", + "command": [ + "pmat", + "analyze", + "satd", + "--format", + "json", + "--path", + "" + ], + "exit_code": 0, + "stdout_sha256": "0f337997eb607f2a757f6c78182c5f1843f2dae49aa9a9dbe4fc8ddcbca088d5", + "failure_scenario": "a TODO or FIXME marker lands unreported", + "precision_class": "advisory" + } + }, + { + "ruleId": "revert_leaves_no_kv_residue", + "level": "note", + "message": { + "text": "b49a662e86 removes only the #4623/#4630 serve-KV fold. The nine non-doc source files it touched (chat_load_tokenizers.rs, serve/handler_gpu_completion.rs, serve/handlers_include_01.rs, serve/mod.rs, api/tests/mod.rs, cuda/gpu_profile.rs, gguf/cuda/mod.rs, gguf/cuda/resident.rs, gguf/cuda/session_kv.rs) are now byte-identical to the merge base: git diff --numstat base..head over them is empty. serve_kv_limit_k1.rs and PMAT-4636.yaml are gone. docs/roadmaps/roadmap.yaml differs from base by only the 21-line PMAT-4650 entry. The three evidence dirs it deletes (4623 x2, 4630 x1) belong to the other PRs." + }, + "properties": { + "grounding": "measured", + "command": [ + "git", + "diff", + "--numstat", + "89ef1b7e35", + "25f354a206", + "--", + "" + ], + "exit_code": 0, + "stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "failure_scenario": "a KV-fold hunk survives the revert and ships under #4711 tools fix", + "precision_class": "advisory" + } + }, + { + "ruleId": "revert_leaves_no_dangling_symbol", + "level": "note", + "message": { + "text": "git grep -w for serving_cuda_model, for_session, session_kv, serve_kv_limit_k1 and PMAT-4636 over crates, docs, contracts and scripts at head matches nothing (rc 1, empty), the same as at the base. fit_serving_context stays at 10 references on both, so the base D5 preflight is untouched." + }, + "properties": { + "grounding": "measured", + "command": [ + "git", + "grep", + "-c", + "-w", + "-e", + "serving_cuda_model", + "-e", + "for_session", + "-e", + "session_kv", + "-e", + "serve_kv_limit_k1", + "-e", + "PMAT-4636", + "25f354a206", + "--", + "crates", + "docs", + "contracts", + "scripts" + ], + "exit_code": 1, + "stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "failure_scenario": "a reference to a removed fn or module breaks the build of aprender-serve or apr-cli", + "precision_class": "advisory" + } + }, + { + "ruleId": "own_files_unchanged_since_last_receipt", + "level": "note", + "message": { + "text": "Every source and test file in the PR diff under crates/ (12 files) is byte-identical at 25f354a206 to f2338256ee, the last reviewed head. The revert and the main merge changed none of #4711 own code, so the earlier review of the tools/template fix, the Qwen XML parser and the extract-fn refactor still describes it." + }, + "properties": { + "grounding": "measured", + "command": [ + "git", + "diff", + "--stat", + "f2338256ee", + "25f354a206", + "--", + "" + ], + "exit_code": 0, + "stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "failure_scenario": "the revert silently altered the tools fix and the earlier review no longer applies", + "precision_class": "advisory" + } + }, + { + "ruleId": "revert_compiles_not_verified", + "level": "note", + "message": { + "text": "No cargo build or test could run on this host, so that the revert compiles, and that the 4650 tests (including qwen35_undeclared_xml_call_does_not_promote_inner_json_4650) are green at head, rest on reading and on the author logs. Reading shows call sites consistent: cuda_chat_backend.rs passes request.tools.as_deref() to tokenize_chat_prompt and to the realize_handlers variant, and no symbol of the removed files is referenced." + }, + "properties": { + "grounding": "asserted", + "rationale": "host policy forbids cargo here; CI workspace-test is the compile check", + "failure_scenario": "revert leaves a signature mismatch that only the compiler sees", + "precision_class": "advisory" + } + } + ] + }, + { + "tool": { + "driver": { + "name": "nvidia-cuda-docs" + } + }, + "invocations": [ + { + "executionSuccessful": true, + "toolExecutionNotifications": [] + } + ], + "results": [] + }, + { + "tool": { + "driver": { + "name": "crux" + } + }, + "results": [ + { + "ruleId": "crux_surface_covered", + "level": "note", + "message": { + "text": "The diff changes the /v1/chat/completions request (tools reach the template) and response (message.tool_calls). contracts/crux-C-11-v1.yaml and contracts/crux-C-03-v1.yaml are present at head. gap_effect none." + }, + "properties": { + "grounding": "measured", + "command": [ + "ls", + "contracts/crux-C-11-v1.yaml", + "contracts/crux-C-03-v1.yaml" + ], + "exit_code": 0, + "stdout_sha256": "89602ee4d43dbf2334a2da7a2254260d0c94cdd4ae913d4f1f8626c6a902322f", + "failure_scenario": "a surface with no covering contract would be crux_coverage none", + "precision_class": "advisory" + } + } + ] + }, + { + "tool": { + "driver": { + "name": "cargo-mutants" + } + }, + "invocations": [ + { + "executionSuccessful": true, + "toolExecutionNotifications": [ + { + "level": "note", + "message": { + "text": "NOT re-run here: host policy runs cargo on another host only. The figures are the GPU-host run recorded at f2338256ee (18 in-diff mutants, 11 caught, 7 missed) and the 12 crates files are byte-identical to that head. The #4623/#4630 mutants of the old diff no longer exist. grammar/tool.rs, chat_template_official.rs and mod_create_demo.rs are include!d and were never enumerated." + } + } + ] + } + ], + "results": [ + { + "ruleId": "mutants_survivors_carried", + "level": "warning", + "message": { + "text": "7 survivors carried from f2338256ee, none re-run: try_gpu_backend and try_cached_backend replacements in openai_handlers.rs (1067, 1185), delete ! in format_chat_messages_official_thinking_tools (realize_handlers.rs 275), two replacements of format_chat_messages_for_state_thinking (323). Rust mutation is advisory under the skill." + }, + "properties": { + "grounding": "asserted", + "rationale": "carried from an earlier receipt over byte-identical files; not measured by this run", + "failure_scenario": "a surviving mutant in the tools-to-template path is a regression no test would catch", + "precision_class": "advisory" + } + } + ] + }, + { + "tool": { + "driver": { + "name": "antigravity" + } + }, + "invocations": [ + { + "executionSuccessful": false, + "toolExecutionNotifications": [ + { + "level": "error", + "message": { + "text": "Consultation could not be performed. The disposable review tree step, git archive 25f354a2063014cc451a315cc3be30d3ed8ade3a | tar -x, was refused by the lane hook subagent-lock.sh (a lane runs read-only git only). agy itself resolves and runs (agy --version = 1.2.17); no review was attempted and the hook was not routed around. Verdict DEGRADED." + }, + "properties": { + "grounding": "measured", + "command": [ + "agy", + "--version" + ], + "exit_code": 0, + "stdout_sha256": "def65f6e126b8289916c66a36d8532fe268897e174531b44e2b3fb1b0022865b" + } + } + ] + } + ], + "results": [] + } + ] +} diff --git a/evidence/pr-review/4711/25f354a2063014cc451a315cc3be30d3ed8ade3a/receipt.intoto.jsonl b/evidence/pr-review/4711/25f354a2063014cc451a315cc3be30d3ed8ade3a/receipt.intoto.jsonl new file mode 100644 index 0000000000..6bc91a9bda --- /dev/null +++ b/evidence/pr-review/4711/25f354a2063014cc451a315cc3be30d3ed8ade3a/receipt.intoto.jsonl @@ -0,0 +1 @@ +{"_type":"https://in-toto.io/Statement/v1","subject":[{"name":"git+https://github.com/paiml/aprender","digest":{"sha1":"25f354a2063014cc451a315cc3be30d3ed8ade3a"}}],"predicateType":"https://paiml.dev/attestations/pr-review/v2","predicate":{"skill_version":"2.1.0","attestation_level":"L1-self","pr":4711,"base_sha":"89ef1b7e3525d77a2e88ba096eeb6137d1af8a52","head_sha":"25f354a2063014cc451a315cc3be30d3ed8ade3a","author_actor":{"kind":"agent","id":"agent:claude-opus-5-5/aprender-w4711"},"reviewer_actor":{"kind":"agent","id":"agent:claude-sonnet-5-5/w4650-review"},"affected_crates":["aprender-serve","apr-cli"],"verdict":"DEGRADED","degraded_reason":"antigravity unreachable: the lane hook refused git archive for the disposable review tree, so agy did not run (agy 1.2.17 resolves; hook not routed around); mutation NOT re-run here (host policy: cargo runs on another host), the 7 survivors of f2338256ee are carried over 12 byte-identical files and the include!d parser files were never enumerated; no cargo build or test ran, so that b49a662e86 compiles and the 4650 tests are green rests on reading and author logs; the cuda-docs-reviewer agent was not dispatched, nvidia-cuda-docs was queried inline once (no authority found); duplication, complexity, tdg and satd were re-measured at head. b49a662e86 review: it removes only the #4623/#4630 KV fold, the nine touched source files equal the merge base, no dangling symbol, no finding on it. No blocking class (B1-B6) fires.","consultations":{"pmat":{"status":"consulted","transport":"cli","transport_unavailable":["mcp: not attempted this run (cli answered)"],"index_commit":"25f354a2063014cc451a315cc3be30d3ed8ade3a","index_is_ancestor":true,"index_worktree_dirty":false,"complexity_delta":[{"file":"crates/aprender-serve/src/api/openai_handlers.rs","function":"build_tool_calling_message","base_cyclomatic":3,"head_cyclomatic":4,"base_cognitive":6,"head_cognitive":7},{"file":"crates/aprender-serve/src/api/mod_create_demo.rs","function":"to_grammar","base_cyclomatic":1,"head_cyclomatic":3,"base_cognitive":0,"head_cognitive":5},{"file":"crates/aprender-serve/src/api/mod_create_demo.rs","function":"extract_tool_parameters","base_cyclomatic":3,"head_cyclomatic":5,"base_cognitive":2,"head_cognitive":6}],"tdg_delta":[{"file":"crates/aprender-serve/src/api/openai_handlers.rs","base":"A-","head":"A-","base_total":87.52,"head_total":86.55},{"file":"crates/aprender-serve/src/api/cuda_chat_backend.rs","base":"B-","head":"B-","base_total":71.12,"head_total":71.02},{"file":"crates/aprender-serve/src/grammar/tool.rs","base":"A","head":"A","base_total":93.5,"head_total":91.0},{"file":"crates/aprender-serve/src/chat_template_official.rs","base":"A","head":"A+","base_total":94.45,"head_total":98.25}],"satd_introduced":[],"cache_hits":0,"analysis_note":"Index built at 25f354a206 in this worktree (93 s, 2054 docs). pmat analyze complexity, tdg and satd re-run over head and base copies of the 9 changed non-test .rs files under crates/ at this head; nothing reused.","duplication_hits":[{"needle":"args_of","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-contracts/src/ontology/extract/cli_surface.rs","line":209,"method":"lexical"},{"needle":"args_of","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-contracts/src/ontology/extract/cli_surface.rs","line":217,"method":"lexical"},{"needle":"args_of","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-contracts/src/ontology/extract/cli_surface.rs","line":232,"method":"lexical"}],"duplication_coverage":{"rust":"semantic","shell":"lexical","python":"lexical","config":"lexical","docs":"lexical","other":"lexical","sibling_branches":"lexical","merge_base_to_main":"lexical"},"duplication_horizon":["head=25f354a2063014cc451a315cc3be30d3ed8ade3a","siblings=refs/remotes/origin/* unmerged into origin/main","merge_base_to_main=89ef1b7e3525d77a2e88ba096eeb6137d1af8a52..refs/remotes/origin/main"],"horizon_branches_total":1478,"horizon_branches_scanned":1478,"merge_base_to_main_files":0,"symbols_searched":35,"queries":[{"q":"tools chat template qwen xml","command":"pmat query \"tools chat template qwen xml\" --limit 5","cwd":"worktree at HEAD","results":5,"note":"top hit render_official_with_tools in this PR; no prior implementation surfaced"}],"duplication_note":"3 hits, all the generic name args_of in aprender-contracts/src/ontology/extract/cli_surface.rs; none is prior art for the tools fix. The old session-KV hits vanished with the revert. merge_base_to_main_files 0; 1478 of 1478 sibling branches swept in 115 s."},"cuda":{"status":"consulted","trigger_reason":"S3.B path trigger fires on crates/aprender-serve/src/api/cuda_chat_backend.rs (path contains cuda). The final diff touches no gguf/cuda file and adds no device-behaviour claim: the extract-fn refactor moves host dispatch code. The cuda-docs-reviewer agent was not dispatched from this lane; one inline query.","queries":[{"q":"CUDA context is bound to the calling host thread; running kernels from a spawned blocking thread requires cuCtxSetCurrent","source_uri":"nvidia-cuda-docs MCP (search_cuda_docs)","result":"no-authority-found","claim_checked":"The refactor keeps dispatch_cuda_stream inside the same blocking task as before, so the thread-current CUDA context is unchanged. The query returned only the threadMigration driver-API sample (cuCtxPushCurrent per host thread) and nothing on spawn_blocking, so no authority was found for or against the claim."}]},"crux":{"status":"consulted","surfaces":["POST /v1/chat/completions request field tools","POST /v1/chat/completions response message.tool_calls and finish_reason"],"contracts":["contracts/crux-C-11-v1.yaml","contracts/crux-C-03-v1.yaml"],"gap_effect":"none","crux_coverage":"covered","comparative_claims":[]},"mutation":{"status":"consulted","scope":"in-diff","attempted":18,"killed":11,"survivors":[{"mutant":"replace try_gpu_backend -> Option with None","file":"crates/aprender-serve/src/api/openai_handlers.rs","line":1067,"killed":false},{"mutant":"replace try_gpu_backend -> Option with Some(Default::default())","file":"crates/aprender-serve/src/api/openai_handlers.rs","line":1067,"killed":false},{"mutant":"replace try_cached_backend -> Option with None","file":"crates/aprender-serve/src/api/openai_handlers.rs","line":1185,"killed":false},{"mutant":"replace try_cached_backend -> Option with Some(Default::default())","file":"crates/aprender-serve/src/api/openai_handlers.rs","line":1185,"killed":false},{"mutant":"delete ! in format_chat_messages_official_thinking_tools","file":"crates/aprender-serve/src/api/realize_handlers.rs","line":275,"killed":false},{"mutant":"replace format_chat_messages_for_state_thinking with Ok(String::new())","file":"crates/aprender-serve/src/api/realize_handlers.rs","line":323,"killed":false},{"mutant":"replace format_chat_messages_for_state_thinking with Ok(\"xyzzy\".into())","file":"crates/aprender-serve/src/api/realize_handlers.rs","line":323,"killed":false}],"note":"carried from the GPU-host run at f2338256ee over byte-identical files; NOT re-run here (host policy)"},"antigravity":{"status":"unreachable","trigger_reason":"the lane hook subagent-lock.sh refused git archive, the S3.E step 3 disposable-tree command (a lane runs read-only git only); agy resolves and runs (agy --version = 1.2.17, rc 0) but no review was run, and the refusal was not routed around"}},"findings_ref":{"path":"findings.sarif","sha256":"1c673f18534603026e7d05f8c9a4363fa7f6162a6c75cf6a9df7b39d6894d742"},"cost":{"input_tokens":260000,"output_tokens":30000,"wall_seconds":1500},"diff_patch_id":"142face2e5f7d3d02999bff43fa9cd7f72f61922","diff_patch_id_algo":"git-patch-id-verbatim/pinned-diff-v1"}} diff --git a/evidence/pr-review/4711/942e3f8fe6a45a005368eec8848762b45b1725b2/findings.sarif b/evidence/pr-review/4711/942e3f8fe6a45a005368eec8848762b45b1725b2/findings.sarif new file mode 100644 index 0000000000..f9384927a2 --- /dev/null +++ b/evidence/pr-review/4711/942e3f8fe6a45a005368eec8848762b45b1725b2/findings.sarif @@ -0,0 +1,307 @@ +{ + "version": "2.1.0", + "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json", + "runs": [ + { + "tool": { + "driver": { + "name": "pmat" + } + }, + "results": [ + { + "ruleId": "complexity_delta", + "level": "note", + "message": { + "text": "pmat analyze complexity over head and base copies of the 17 changed non-test .rs files. Only five functions rise: build_tool_calling_message 3->4, to_grammar 1->3, extract_tool_parameters 3->5, gguf/cuda/mod.rs build 6->8 (from #4623), serve run 6->7; all under 10. The extract-fn refactor lowers try_cuda_backend from cyclomatic 14 / cognitive 43 to 9 / 20 and completions_inner from 9 / 26 to 8 / 21." + }, + "properties": { + "grounding": "measured", + "command": [ + "pmat", + "analyze", + "complexity", + "--format", + "json", + "--path", + "" + ], + "exit_code": 0, + "stdout_sha256": "d811c87edb1a71e8ad5a404cb618a58cae0cb7b704af4be14f5efed8f5f92017", + "failure_scenario": "a later branch in extract_tool_parameters or try_cuda_backend crosses cyclomatic 10", + "precision_class": "advisory" + } + }, + { + "ruleId": "tdg_delta", + "level": "note", + "message": { + "text": "pmat analyze tdg: no changed file drops a grade. handler_gpu_completion.rs 89.79 -> 88.52, still A-. grammar/tool.rs and gguf/cuda/mod.rs have no base copy in the 17-file set (A at head)." + }, + "properties": { + "grounding": "measured", + "command": [ + "pmat", + "analyze", + "tdg", + "--format", + "json", + "--path", + "" + ], + "exit_code": 0, + "stdout_sha256": "e22114fee548d3aaf38120f9fc90d606f151177cd5e987d43c616a0e0f671442", + "failure_scenario": "none: grade unchanged", + "precision_class": "advisory" + } + }, + { + "ruleId": "satd_none_introduced", + "level": "note", + "message": { + "text": "pmat analyze satd over the 17 head files and the base copies: 0 violations on both." + }, + "properties": { + "grounding": "measured", + "command": [ + "pmat", + "analyze", + "satd", + "--format", + "json", + "--path", + "" + ], + "exit_code": 0, + "stdout_sha256": "4c677a9e5f190ffaa95b108f941bf34ba9f30a27c1aea16cbfbad1ac4f325e28", + "failure_scenario": "a TODO added by this diff would be counted", + "precision_class": "advisory" + } + }, + { + "ruleId": "refactor_equivalence_try_cuda_backend", + "level": "note", + "message": { + "text": "Commit 26ccc7f932 extracts dispatch_cuda_stream, cuda_batch_collect, cuda_direct_collect and try_cuda_direct_completions. Read line by line: the 503 batch-queue-full path keeps record_admission_rejected, the 500 and generation_error_status paths are unchanged, timing_tx sends are unchanged, and chain order is unchanged. Two clones became moves. No behaviour change found." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/aprender-serve/src/api/cuda_chat_backend.rs" + }, + "region": { + "startLine": 1 + } + } + } + ], + "properties": { + "grounding": "asserted", + "rationale": "Read by the primary against the base copy; no cargo test could be run on this host, and the extracted paths are device paths.", + "failure_scenario": "a dropped record_admission_rejected would under-count 503s with no test failing", + "precision_class": "advisory" + } + }, + { + "ruleId": "qwen_xml_fallback_preempted_by_json", + "level": "warning", + "message": { + "text": "build_tool_calling_message runs the default parser first and tries QwenXml only when it returns no calls. A Qwen XML call whose parameter value contains JSON with name and arguments keys is taken as the call, and the real tool name is discarded. Low likelihood, wrong tool executed when it fires." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/aprender-serve/src/api/openai_handlers.rs" + }, + "region": { + "startLine": 643 + } + } + } + ], + "properties": { + "grounding": "asserted", + "rationale": "Read of the two parse calls at lines 642-650. agy raised the same defect independently. Not run.", + "failure_scenario": "model emits {\"name\":\"rm\",\"arguments\":{}} and the response carries a call to rm", + "precision_class": "advisory" + } + }, + { + "ruleId": "qwen_xml_parser_edge_cases", + "level": "note", + "message": { + "text": "parse_qwen_xml finds or at end of text, and does not skip think blocks. A call quoted inside reasoning, a truncated final call, or a literal inside a string value parse as a call or cut the value short." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/aprender-serve/src/grammar/tool.rs" + }, + "region": { + "startLine": 296 + } + } + } + ], + "properties": { + "grounding": "asserted", + "rationale": "Read of parse_qwen_xml, qwen_xml_arguments and qwen_xml_value. The module is include!d so no mutation or unit run covers it from this host.", + "failure_scenario": "a value containing is truncated at that tag and the rest is dropped", + "precision_class": "advisory" + } + }, + { + "ruleId": "legacy_template_tools_warning_noise", + "level": "note", + "message": { + "text": "The legacy template path prints an eprintln! warning on every request that carries tools. Honest, but it is stderr noise per request and not a tracing event." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/aprender-serve/src/api/realize_handlers.rs" + }, + "region": { + "startLine": 287 + } + } + } + ], + "properties": { + "grounding": "asserted", + "rationale": "Read; not run.", + "failure_scenario": "a client sending tools on every turn fills the server log", + "precision_class": "advisory" + } + } + ] + }, + { + "tool": { + "driver": { + "name": "nvidia-cuda-docs" + } + }, + "invocations": [ + { + "executionSuccessful": true, + "toolExecutionNotifications": [] + } + ], + "results": [] + }, + { + "tool": { + "driver": { + "name": "crux" + } + }, + "results": [ + { + "ruleId": "crux_surface_covered", + "level": "note", + "message": { + "text": "The #4650 half changes the /v1/chat/completions request (tools reach the chat template) and the response (message.tool_calls for Qwen3.5 XML output). contracts/crux-C-11-v1.yaml (OpenAI tool-use function calling) and contracts/crux-C-03-v1.yaml (finish_reason incl. tool_calls) exist at head. gap_effect none. The Qwen XML shape is an extension below the contract's OpenAI-shaped response and is not itself named in it." + }, + "properties": { + "grounding": "measured", + "command": [ + "ls", + "contracts/crux-C-11-v1.yaml", + "contracts/crux-C-03-v1.yaml" + ], + "exit_code": 0, + "stdout_sha256": "d29327d42a930adfad7c93554099c390e32a0767cbaf5b1a60769e61bdca9e5a", + "failure_scenario": "A surface with no covering contract would be crux_coverage none; both contracts are present.", + "precision_class": "advisory" + } + } + ] + }, + { + "tool": { + "driver": { + "name": "cargo-mutants" + } + }, + "invocations": [ + { + "executionSuccessful": true, + "toolExecutionNotifications": [ + { + "level": "note", + "message": { + "text": "Ran on gx10 only: cargo mutants -p aprender-serve --in-diff --jobs 1 --cargo-arg=--lib, test set filtered to 4650 pmat801 chat_template_wiring thinking_on_refusal chat_template_official format_chat. 18 mutants, 11 caught, 7 missed, 0 unviable, 0 timeout. grammar/tool.rs, chat_template_official.rs and mod_create_demo.rs are include!d and cargo-mutants did not enumerate them, so the parser and the tojson filter went unmutated." + } + } + ] + } + ], + "results": [ + { + "ruleId": "mutants_survivors_in_diff", + "level": "warning", + "message": { + "text": "7 of 18 in-diff mutants survived: try_gpu_backend -> None and Some(Default::default()) (openai_handlers.rs:1067), try_cached_backend -> None and Some(Default::default()) (openai_handlers.rs:1185), delete ! in format_chat_messages_official_thinking_tools (realize_handlers.rs:275), and format_chat_messages_for_state_thinking -> Ok(String::new()) and Ok(\"xyzzy\") (realize_handlers.rs:323). The test filter limited which tests could kill them, so some may be killed by the full lib suite. The realize_handlers.rs:275 survivor is the empty-tools guard of the #4650 fix." + }, + "properties": { + "grounding": "measured", + "command": [ + "cargo", + "mutants", + "-p", + "aprender-serve", + "--in-diff", + "", + "--jobs", + "1", + "--cargo-arg=--lib" + ], + "exit_code": 2, + "stdout_sha256": "4580a915a8c1438873474bf135d4f078c0c79913aff45be31cf2b23ba076ee35", + "failure_scenario": "deleting the ! at realize_handlers.rs:275 makes the filter keep empty tool lists and render an empty tools block, and no filtered test fails", + "precision_class": "advisory" + } + } + ] + }, + { + "tool": { + "driver": { + "name": "antigravity" + } + }, + "results": [ + { + "ruleId": "agy_qwen_xml_preempted", + "level": "warning", + "message": { + "text": "The default OpenAI parser runs before the QwenXml parser and extracts any embedded JSON block containing name and arguments, skipping the XML call. Cited line 679 does not hold the two parse calls; they sit at 642-650." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/aprender-serve/src/api/openai_handlers.rs" + }, + "region": { + "startLine": 679 + } + } + } + ], + "properties": { + "grounding": "asserted", + "rationale": "Raised by antigravity as asserted. The primary reached the same defect independently (qwen_xml_fallback_preempted_by_json) and re-read lines 642-650; it agrees on the defect and corrects the line.", + "failure_scenario": "a Qwen XML call with JSON in a parameter value is returned as the inner JSON call", + "precision_class": "advisory" + } + } + ] + } + ] +} diff --git a/evidence/pr-review/4711/942e3f8fe6a45a005368eec8848762b45b1725b2/receipt.intoto.jsonl b/evidence/pr-review/4711/942e3f8fe6a45a005368eec8848762b45b1725b2/receipt.intoto.jsonl new file mode 100644 index 0000000000..389e10eac1 --- /dev/null +++ b/evidence/pr-review/4711/942e3f8fe6a45a005368eec8848762b45b1725b2/receipt.intoto.jsonl @@ -0,0 +1 @@ +{"_type":"https://in-toto.io/Statement/v1","subject":[{"name":"git+https://github.com/paiml/aprender","digest":{"sha1":"942e3f8fe6a45a005368eec8848762b45b1725b2"}}],"predicateType":"https://paiml.dev/attestations/pr-review/v2","predicate":{"skill_version":"2.1.0","attestation_level":"L1-self","pr":4711,"base_sha":"11f844a772f77647d7d4b6281acbb2ab5e0bd76b","head_sha":"942e3f8fe6a45a005368eec8848762b45b1725b2","author_actor":{"kind":"agent","id":"agent:claude-opus-5-5/aprender-w4711"},"reviewer_actor":{"kind":"agent","id":"agent:claude-sonnet-5-5/w4711-review"},"affected_crates":["aprender-serve","apr-cli"],"verdict":"DEGRADED","degraded_reason":"mutation ran on gx10 only with a filtered test set and left 7 of 18 in-diff mutants surviving; the include!d files grammar/tool.rs, chat_template_official.rs and mod_create_demo.rs are invisible to cargo-mutants, so the Qwen XML parser and the tojson filter were not mutated; no cargo test or device run was possible on this host, so the extract-fn refactor equivalence rests on reading; the cuda-docs-reviewer agent could not be dispatched from this lane so nvidia-cuda-docs was queried inline once; origin/main could not be refreshed (fetch blocked), the local origin/main equals base; agy returned a schema-valid structured_output but its status was ERROR (503 on attempt 1); affected_crates was derived from file paths, not cargo metadata. No blocking class (B1-B6) fires.","consultations":{"pmat":{"status":"consulted","transport":"cli","transport_unavailable":["mcp: not attempted this run (cli answered)"],"index_commit":"942e3f8fe6a45a005368eec8848762b45b1725b2","index_is_ancestor":true,"index_worktree_dirty":false,"complexity_delta":[{"file":"crates/aprender-serve/src/api/openai_handlers.rs","function":"build_tool_calling_message","base_cyclomatic":3,"head_cyclomatic":4,"base_cognitive":6,"head_cognitive":7},{"file":"crates/aprender-serve/src/gguf/cuda/mod.rs","function":"build","base_cyclomatic":6,"head_cyclomatic":8,"base_cognitive":11,"head_cognitive":13},{"file":"crates/apr-cli/src/commands/serve/mod.rs","function":"run","base_cyclomatic":6,"head_cyclomatic":7,"base_cognitive":7,"head_cognitive":8},{"file":"crates/aprender-serve/src/api/mod_create_demo.rs","function":"to_grammar","base_cyclomatic":1,"head_cyclomatic":3,"base_cognitive":0,"head_cognitive":5},{"file":"crates/aprender-serve/src/api/mod_create_demo.rs","function":"extract_tool_parameters","base_cyclomatic":3,"head_cyclomatic":5,"base_cognitive":2,"head_cognitive":6}],"tdg_delta":[{"file":"crates/apr-cli/src/commands/serve/handler_gpu_completion.rs","base":"A-","head":"A-","base_total":89.79,"head_total":88.52},{"file":"crates/aprender-serve/src/grammar/tool.rs","base":null,"head":"A","base_total":null,"head_total":91.0},{"file":"crates/aprender-serve/src/gguf/cuda/mod.rs","base":null,"head":"A","base_total":null,"head_total":90.0}],"satd_introduced":[],"cache_hits":0,"analysis_note":"pmat analyze complexity, satd and tdg over head and base copies of the 17 changed non-test .rs files. 0 SATD on both. Complexity of try_cuda_backend falls 14/43 -> 9/20 and completions_inner 9/26 -> 8/21. Scores are per copy and context dependent; files without a base copy show base null.","duplication_hits":[{"needle":"args_of","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-contracts/src/ontology/extract/cli_surface.rs","line":209,"method":"lexical"},{"needle":"args_of","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-contracts/src/ontology/extract/cli_surface.rs","line":217,"method":"lexical"},{"needle":"args_of","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-contracts/src/ontology/extract/cli_surface.rs","line":232,"method":"lexical"},{"needle":"fp16_cache","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-serve/src/gpu/streaming_kv_streaming.rs","line":278,"method":"lexical"},{"needle":"fp16_cache","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-serve/src/gpu/streaming_kv_streaming.rs","line":281,"method":"lexical"},{"needle":"fp16_cache","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-serve/tests/gpu_coverage.rs","line":1552,"method":"lexical"},{"needle":"fp16_cache","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-serve/tests/gpu_coverage.rs","line":1554,"method":"lexical"},{"needle":"resolve_serve_max_seq_len","kind":"name","where":"HEAD","ref":"HEAD","path":"evidence/serve/3791/Q4K.md","line":23,"method":"lexical"},{"needle":"serve_kv_limit_k1.rs","kind":"filename","where":"branch","ref":"origin/devont/k1-revert-red","path":"crates/aprender-serve/src/api/tests/serve_kv_limit_k1.rs","line":0,"method":"lexical"},{"needle":"session_kv.rs","kind":"filename","where":"branch","ref":"origin/devont/k1-revert-red","path":"crates/aprender-serve/src/gguf/cuda/session_kv.rs","line":0,"method":"lexical"},{"needle":"session_kv.rs","kind":"filename","where":"branch","ref":"origin/ec/3715-chunked-prefill","path":"crates/aprender-serve/src/gguf/cuda/session_kv.rs","line":0,"method":"lexical"},{"needle":"serve_kv_limit_k1.rs","kind":"filename","where":"branch","ref":"origin/pr/4631","path":"crates/aprender-serve/src/api/tests/serve_kv_limit_k1.rs","line":0,"method":"lexical"},{"needle":"session_kv.rs","kind":"filename","where":"branch","ref":"origin/pr/4631","path":"crates/aprender-serve/src/gguf/cuda/session_kv.rs","line":0,"method":"lexical"},{"needle":"serve_kv_limit_k1.rs","kind":"filename","where":"branch","ref":"origin/verify/6a-4606","path":"crates/aprender-serve/src/api/tests/serve_kv_limit_k1.rs","line":0,"method":"lexical"},{"needle":"session_kv.rs","kind":"filename","where":"branch","ref":"origin/verify/6a-4606","path":"crates/aprender-serve/src/gguf/cuda/session_kv.rs","line":0,"method":"lexical"},{"needle":"PMAT-4636.yaml","kind":"filename","where":"branch","ref":"origin/verify/6a-4606","path":"docs/roadmaps/entries/PMAT-4636.yaml","line":0,"method":"lexical"}],"duplication_coverage":{"rust":"semantic","shell":"lexical","python":"lexical","config":"lexical","docs":"lexical","other":"lexical","sibling_branches":"lexical","merge_base_to_main":"lexical"},"duplication_horizon":["head=942e3f8fe6a45a005368eec8848762b45b1725b2","siblings=refs/remotes/origin/* unmerged into origin/main","merge_base_to_main=11f844a772f77647d7d4b6281acbb2ab5e0bd76b..refs/remotes/origin/main"],"horizon_branches_total":1406,"horizon_branches_scanned":1406,"merge_base_to_main_files":0,"symbols_searched":71,"queries":[{"q":"qwen xml tool call parser function parameter","command":"pmat query \"qwen xml tool call parser function parameter\" --limit 3","cwd":"worktree at HEAD","results":3,"note":"top hits are parse_qwen_xml and qwen_xml_arguments in this PR; no prior implementation surfaced"}],"duplication_note":"all 16 hits are the K1/session-KV files from the merged #4623 on sibling branches, generic fp16_cache and args_of names, or an evidence page; none is landed prior art for the tools fix (merge_base_to_main_files 0)."},"cuda":{"status":"consulted","trigger_reason":"S3.B path trigger fires on api/cuda_chat_backend.rs and the #4623 files under gguf/cuda/ (path contains cuda). The diff adds no device-behaviour claim: the extract-fn refactor moves host code, and #4623 carries its own queries. The cuda-docs-reviewer agent could not be dispatched from this lane, so the primary queried the nvidia-cuda-docs MCP inline once.","queries":[{"q":"CUDA context bound to calling thread when running GPU kernels from a spawned blocking thread; cuCtxSetCurrent","source_uri":"nvidia-cuda-docs MCP (search_cuda_docs)","result":"no-authority-found","claim_checked":"The refactor keeps dispatch_cuda_stream inside the same blocking task as before, so the thread-current CUDA context is unchanged. The query returned the threadMigration driver-API sample (cuCtxPushCurrent per host thread) and nothing on spawn_blocking, so no authority was found for or against the claim."}]},"crux":{"status":"consulted","surfaces":["POST /v1/chat/completions request field tools","POST /v1/chat/completions response message.tool_calls and finish_reason"],"contracts":["contracts/crux-C-11-v1.yaml","contracts/crux-C-03-v1.yaml"],"gap_effect":"none","crux_coverage":"covered","comparative_claims":[]},"mutation":{"status":"consulted","scope":"in-diff","attempted":18,"killed":11,"survivors":[{"mutant":"replace try_gpu_backend -> Option with None","file":"crates/aprender-serve/src/api/openai_handlers.rs","line":1067,"killed":false},{"mutant":"replace try_gpu_backend -> Option with Some(Default::default())","file":"crates/aprender-serve/src/api/openai_handlers.rs","line":1067,"killed":false},{"mutant":"replace try_cached_backend -> Option with None","file":"crates/aprender-serve/src/api/openai_handlers.rs","line":1185,"killed":false},{"mutant":"replace try_cached_backend -> Option with Some(Default::default())","file":"crates/aprender-serve/src/api/openai_handlers.rs","line":1185,"killed":false},{"mutant":"delete ! in format_chat_messages_official_thinking_tools","file":"crates/aprender-serve/src/api/realize_handlers.rs","line":275,"killed":false},{"mutant":"replace format_chat_messages_for_state_thinking with Ok(String::new())","file":"crates/aprender-serve/src/api/realize_handlers.rs","line":323,"killed":false},{"mutant":"replace format_chat_messages_for_state_thinking with Ok(\"xyzzy\".into())","file":"crates/aprender-serve/src/api/realize_handlers.rs","line":323,"killed":false}]},"antigravity":{"status":"consulted","attempted":1,"agy_version":"agy 1.2.17","binary_path":"/home/noah/.local/bin/agy","model_id":"gemini-3.1-pro-high","model_family":"google/gemini","exit_code":0,"duration_seconds":516.96,"agy_status":"ERROR","usage":{"input_tokens":337713,"output_tokens":42491,"thinking_tokens":34119,"cache_read_tokens":2799448,"total_tokens":380204},"output_check":{"structured_output_present":true,"reviewed":true,"schema_valid":true},"schema_check":"check-jsonschema --schemafile .claude/skills/pr-review/agy-review-v1.schema.json : ok","reverified_by_primary":false,"divergence_note":"agreed = the Qwen XML fallback is pre-empted by the default JSON parser (the primary re-read lines 642-650 and agrees on the defect; agy cites line 679, the code is at 643). primary_only = 8 results: complexity delta, tdg delta, satd, refactor equivalence, parser edge cases, legacy-template warning noise, crux coverage, mutation survivors. agy status was ERROR with an API 503 on attempt 1, yet the run exited 0 and returned a reviewed, schema-valid structured_output, so it is recorded as consulted with the status as a diagnostic.","divergence":{"agreed":1,"agy_only":0,"primary_only":8,"contradicted":0},"findings":[{"id":"qwen_xml_preempted_by_default_parser","file":"crates/aprender-serve/src/api/openai_handlers.rs","grounding":"asserted"}]}},"findings_ref":{"path":"findings.sarif","sha256":"dcf393efaaad2571bb8e7828a631a3960c98e4ee35fbfb3370b6e3ca8a7d5cec"},"cost":{"input_tokens":337713,"output_tokens":42491,"wall_seconds":3600},"diff_patch_id":"ac23765bd18a072d55b099abd72d809b684270c6","diff_patch_id_algo":"git-patch-id-verbatim/pinned-diff-v1"}} diff --git a/evidence/pr-review/4711/e95438e370fed728ba10b78eb07e088fe2bc8097/agy.json b/evidence/pr-review/4711/e95438e370fed728ba10b78eb07e088fe2bc8097/agy.json new file mode 100644 index 0000000000..8e314350b6 --- /dev/null +++ b/evidence/pr-review/4711/e95438e370fed728ba10b78eb07e088fe2bc8097/agy.json @@ -0,0 +1 @@ +{"conversation_id":"8f6a0cbd-d781-4d48-94cd-9dd07145050d","status":"SUCCESS","response":"{\"findings\":[{\"failure_scenario\":\"For `apr serve` models, `KvLen::FitServing` provisions `1 + SERVE_BATCH_SLOTS` KV slots per sequence to accommodate continuous batching. However, the `need` calculation only adds `r.kv_per_pos * max_seq_len` (1 slot) to the footprint. This severely underestimates VRAM usage. As a result, the check incorrectly concludes that the FP16 batched prefill cache will fit alongside the weights and KV caches, leading to an OOM crash or degraded throughput when the scheduler allocates the remaining KV slots.\",\"file\":\"crates/aprender-serve/src/gguf/cuda/mod.rs\",\"grounding\":\"asserted\",\"id\":\"fit_check_need_ignores_serve_batched_slots\",\"line\":837,\"summary\":\"The `need` calculation for the FP16 batched prefill fit check does not account for `SERVE_BATCH_SLOTS` when `KvLen::FitServing` is used.\"},{\"failure_scenario\":\"If `cudaMemGetInfo` returns an error (e.g., context lost), `build()` proceeds with `free=0`, picks the 2048-position floor, prints `0.0 GB is free` and forces `Serial` prefill. The chat session becomes slow and short for a reason the log misattributes to VRAM.\",\"file\":\"crates/aprender-serve/src/gguf/cuda/mod.rs\",\"grounding\":\"asserted\",\"id\":\"memory_probe_failure_reads_as_zero_free\",\"line\":820,\"summary\":\"`memory_info().unwrap_or((0, 0))` reads a failed memory probe as zero free VRAM and silently forces Serial prefill.\"},{\"failure_scenario\":\"When a QK-norm model cannot fit the FP16 batched prefill in VRAM, `session_kv_len` clamps the context length to a floor of 2048 because it subtracts the ~4.6GB batched reserve. `serial_when_fp16_prefill_does_not_fit` then switches the prefill path to `Serial`. However, `max_seq_len` is never recalculated for the `Serial` path (which does not need the full reserve). As a result, the session is artificially limited to 2048 tokens and will time out or fall back to CPU on long chat turns, wasting VRAM that could have supported more tokens.\",\"file\":\"crates/aprender-serve/src/gguf/cuda/mod.rs\",\"grounding\":\"asserted\",\"id\":\"kv_not_recomputed_after_serial_fallback\",\"line\":838,\"summary\":\"After the fit check switches to `Serial` prefill, `max_seq_len` is not recomputed, wasting available VRAM.\"},{\"failure_scenario\":\"An operator sets `BATCHED_PREFILL=0`, reads the `FP16 batched prefill` line in the log and concludes batched prefill is active. They chase a `Serial`-path slowdown in the wrong place.\",\"file\":\"crates/aprender-serve/src/gguf/cuda/mod.rs\",\"grounding\":\"asserted\",\"id\":\"fp16_message_printed_on_forced_serial\",\"line\":792,\"summary\":\"The `[#3413] FP16 batched prefill` message prints even when the path is `Serial` because `BATCHED_PREFILL=0` was set explicitly.\"}],\"reviewed\":true,\"toolAction\":\"Reporting review findings\",\"toolSummary\":\"Report findings\"}\n","duration_seconds":106.844678336,"num_turns":1,"structured_output":{"findings":[{"failure_scenario":"For `apr serve` models, `KvLen::FitServing` provisions `1 + SERVE_BATCH_SLOTS` KV slots per sequence to accommodate continuous batching. However, the `need` calculation only adds `r.kv_per_pos * max_seq_len` (1 slot) to the footprint. This severely underestimates VRAM usage. As a result, the check incorrectly concludes that the FP16 batched prefill cache will fit alongside the weights and KV caches, leading to an OOM crash or degraded throughput when the scheduler allocates the remaining KV slots.","file":"crates/aprender-serve/src/gguf/cuda/mod.rs","grounding":"asserted","id":"fit_check_need_ignores_serve_batched_slots","line":837,"summary":"The `need` calculation for the FP16 batched prefill fit check does not account for `SERVE_BATCH_SLOTS` when `KvLen::FitServing` is used."},{"failure_scenario":"If `cudaMemGetInfo` returns an error (e.g., context lost), `build()` proceeds with `free=0`, picks the 2048-position floor, prints `0.0 GB is free` and forces `Serial` prefill. The chat session becomes slow and short for a reason the log misattributes to VRAM.","file":"crates/aprender-serve/src/gguf/cuda/mod.rs","grounding":"asserted","id":"memory_probe_failure_reads_as_zero_free","line":820,"summary":"`memory_info().unwrap_or((0, 0))` reads a failed memory probe as zero free VRAM and silently forces Serial prefill."},{"failure_scenario":"When a QK-norm model cannot fit the FP16 batched prefill in VRAM, `session_kv_len` clamps the context length to a floor of 2048 because it subtracts the ~4.6GB batched reserve. `serial_when_fp16_prefill_does_not_fit` then switches the prefill path to `Serial`. However, `max_seq_len` is never recalculated for the `Serial` path (which does not need the full reserve). As a result, the session is artificially limited to 2048 tokens and will time out or fall back to CPU on long chat turns, wasting VRAM that could have supported more tokens.","file":"crates/aprender-serve/src/gguf/cuda/mod.rs","grounding":"asserted","id":"kv_not_recomputed_after_serial_fallback","line":838,"summary":"After the fit check switches to `Serial` prefill, `max_seq_len` is not recomputed, wasting available VRAM."},{"failure_scenario":"An operator sets `BATCHED_PREFILL=0`, reads the `FP16 batched prefill` line in the log and concludes batched prefill is active. They chase a `Serial`-path slowdown in the wrong place.","file":"crates/aprender-serve/src/gguf/cuda/mod.rs","grounding":"asserted","id":"fp16_message_printed_on_forced_serial","line":792,"summary":"The `[#3413] FP16 batched prefill` message prints even when the path is `Serial` because `BATCHED_PREFILL=0` was set explicitly."}],"reviewed":true},"json_schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://paiml.dev/schemas/pr-review/agy-review-v1.json","title":"PR-REVIEW-SKILL-002 v2 §3.E — the structured output agy is asked for","$comment":"NOT under schemas/. That directory is VENDORED UPSTREAM artifacts only — two files, a SHA-256 manifest and a sources.json recording where each came from, enforced by scripts/check_vendored_schemas.sh and its own mutation guard. This schema is authored here, has no upstream, and would have no honest row in sources.json; putting it there would make a provenance record that records nothing. It is the skill's own output contract, so it lives with the skill.","type":"object","additionalProperties":false,"required":["reviewed","findings"],"properties":{"reviewed":{"type":"boolean","description":"false means agy declined or could not review. The caller records status: unreachable and verdict DEGRADED — never a run that found nothing."},"findings":{"type":"array","description":"May be empty. An empty array under reviewed:true is 'ran and found nothing', which is a different artifact from 'could not run' (§3.0).","items":{"type":"object","additionalProperties":false,"required":["id","grounding","summary","failure_scenario"],"properties":{"id":{"type":"string","minLength":1},"grounding":{"enum":["cited","measured","asserted"],"description":"§1's three marks and there is no fourth. A `measured` finding here was measured by AGY's process, not the primary reviewer's — the receipt records that in reverified_by_primary (§3.E.5)."},"summary":{"type":"string","minLength":1},"failure_scenario":{"type":"string","minLength":1,"description":"§4.2: a finding that cannot name the concrete failure it permits is a comment, not a finding."},"file":{"type":"string"},"line":{"type":"integer","minimum":1},"source":{"type":"string","description":"Required by the guard when grounding is `cited`: a citation with no source is an assertion wearing the mark of a citation (§1.1)."},"command":{"type":"array","items":{"type":"string"},"description":"argv, when grounding is `measured`. It records what AGY ran, and the primary reviewer does not re-run it (§3.E.5)."}}}}}},"usage":{"input_tokens":94004,"output_tokens":11280,"thinking_tokens":8867,"cache_read_tokens":967539,"total_tokens":105284}} diff --git a/evidence/pr-review/4711/e95438e370fed728ba10b78eb07e088fe2bc8097/findings.sarif b/evidence/pr-review/4711/e95438e370fed728ba10b78eb07e088fe2bc8097/findings.sarif new file mode 100644 index 0000000000..f18cc424c8 --- /dev/null +++ b/evidence/pr-review/4711/e95438e370fed728ba10b78eb07e088fe2bc8097/findings.sarif @@ -0,0 +1,382 @@ +{ + "version": "2.1.0", + "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json", + "runs": [ + { + "tool": { + "driver": { + "name": "pmat" + } + }, + "results": [ + { + "ruleId": "complexity_delta", + "level": "note", + "message": { + "text": "pmat analyze complexity over head and base copies of the 17 changed non-test .rs files. Only five functions rise: build_tool_calling_message 3->4, to_grammar 1->3, extract_tool_parameters 3->5, gguf/cuda/mod.rs build 6->8 (from #4623), serve run 6->7; all under 10. The extract-fn refactor lowers try_cuda_backend from cyclomatic 14 / cognitive 43 to 9 / 20 and completions_inner from 9 / 26 to 8 / 21." + }, + "properties": { + "grounding": "measured", + "command": [ + "pmat", + "analyze", + "complexity", + "--format", + "json", + "--path", + "" + ], + "exit_code": 0, + "stdout_sha256": "d811c87edb1a71e8ad5a404cb618a58cae0cb7b704af4be14f5efed8f5f92017", + "failure_scenario": "a later branch in extract_tool_parameters or try_cuda_backend crosses cyclomatic 10", + "precision_class": "advisory" + } + }, + { + "ruleId": "tdg_delta", + "level": "note", + "message": { + "text": "pmat analyze tdg: no changed file drops a grade. handler_gpu_completion.rs 89.79 -> 88.52, still A-. grammar/tool.rs and gguf/cuda/mod.rs have no base copy in the 17-file set (A at head)." + }, + "properties": { + "grounding": "measured", + "command": [ + "pmat", + "analyze", + "tdg", + "--format", + "json", + "--path", + "" + ], + "exit_code": 0, + "stdout_sha256": "e22114fee548d3aaf38120f9fc90d606f151177cd5e987d43c616a0e0f671442", + "failure_scenario": "none: grade unchanged", + "precision_class": "advisory" + } + }, + { + "ruleId": "satd_none_introduced", + "level": "note", + "message": { + "text": "pmat analyze satd over the 17 head files and the base copies: 0 violations on both." + }, + "properties": { + "grounding": "measured", + "command": [ + "pmat", + "analyze", + "satd", + "--format", + "json", + "--path", + "" + ], + "exit_code": 0, + "stdout_sha256": "4c677a9e5f190ffaa95b108f941bf34ba9f30a27c1aea16cbfbad1ac4f325e28", + "failure_scenario": "a TODO added by this diff would be counted", + "precision_class": "advisory" + } + }, + { + "ruleId": "refactor_equivalence_try_cuda_backend", + "level": "note", + "message": { + "text": "Commit 26ccc7f932 extracts dispatch_cuda_stream, cuda_batch_collect, cuda_direct_collect and try_cuda_direct_completions. Read line by line: the 503 batch-queue-full path keeps record_admission_rejected, the 500 and generation_error_status paths are unchanged, timing_tx sends are unchanged, and chain order is unchanged. Two clones became moves. No behaviour change found." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/aprender-serve/src/api/cuda_chat_backend.rs" + }, + "region": { + "startLine": 1 + } + } + } + ], + "properties": { + "grounding": "asserted", + "rationale": "Read by the primary against the base copy; no cargo test could be run on this host, and the extracted paths are device paths.", + "failure_scenario": "a dropped record_admission_rejected would under-count 503s with no test failing", + "precision_class": "advisory" + } + }, + { + "ruleId": "qwen_xml_fallback_preempted_by_json", + "level": "warning", + "message": { + "text": "build_tool_calling_message runs the default parser first and tries QwenXml only when it returns no calls. A Qwen XML call whose parameter value contains JSON with name and arguments keys is taken as the call, and the real tool name is discarded. Low likelihood, wrong tool executed when it fires." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/aprender-serve/src/api/openai_handlers.rs" + }, + "region": { + "startLine": 643 + } + } + } + ], + "properties": { + "grounding": "asserted", + "rationale": "Read of the two parse calls at lines 642-650. agy raised the same defect independently. Not run.", + "failure_scenario": "model emits {\"name\":\"rm\",\"arguments\":{}} and the response carries a call to rm", + "precision_class": "advisory" + } + }, + { + "ruleId": "qwen_xml_parser_edge_cases", + "level": "note", + "message": { + "text": "parse_qwen_xml finds or at end of text, and does not skip think blocks. A call quoted inside reasoning, a truncated final call, or a literal inside a string value parse as a call or cut the value short." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/aprender-serve/src/grammar/tool.rs" + }, + "region": { + "startLine": 296 + } + } + } + ], + "properties": { + "grounding": "asserted", + "rationale": "Read of parse_qwen_xml, qwen_xml_arguments and qwen_xml_value. The module is include!d so no mutation or unit run covers it from this host.", + "failure_scenario": "a value containing is truncated at that tag and the rest is dropped", + "precision_class": "advisory" + } + }, + { + "ruleId": "legacy_template_tools_warning_noise", + "level": "note", + "message": { + "text": "The legacy template path prints an eprintln! warning on every request that carries tools. Honest, but it is stderr noise per request and not a tracing event." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/aprender-serve/src/api/realize_handlers.rs" + }, + "region": { + "startLine": 287 + } + } + } + ], + "properties": { + "grounding": "asserted", + "rationale": "Read; not run.", + "failure_scenario": "a client sending tools on every turn fills the server log", + "precision_class": "advisory" + } + } + ] + }, + { + "tool": { + "driver": { + "name": "nvidia-cuda-docs" + } + }, + "invocations": [ + { + "executionSuccessful": true, + "toolExecutionNotifications": [] + } + ], + "results": [] + }, + { + "tool": { + "driver": { + "name": "crux" + } + }, + "results": [ + { + "ruleId": "crux_surface_covered", + "level": "note", + "message": { + "text": "The #4650 half changes the /v1/chat/completions request (tools reach the chat template) and the response (message.tool_calls for Qwen3.5 XML output). contracts/crux-C-11-v1.yaml (OpenAI tool-use function calling) and contracts/crux-C-03-v1.yaml (finish_reason incl. tool_calls) exist at head. gap_effect none. The Qwen XML shape is an extension below the contract's OpenAI-shaped response and is not itself named in it." + }, + "properties": { + "grounding": "measured", + "command": [ + "ls", + "contracts/crux-C-11-v1.yaml", + "contracts/crux-C-03-v1.yaml" + ], + "exit_code": 0, + "stdout_sha256": "d29327d42a930adfad7c93554099c390e32a0767cbaf5b1a60769e61bdca9e5a", + "failure_scenario": "A surface with no covering contract would be crux_coverage none; both contracts are present.", + "precision_class": "advisory" + } + } + ] + }, + { + "tool": { + "driver": { + "name": "cargo-mutants" + } + }, + "invocations": [ + { + "executionSuccessful": true, + "toolExecutionNotifications": [ + { + "level": "note", + "message": { + "text": "Ran on gx10 only: cargo mutants -p aprender-serve --in-diff --jobs 1 --cargo-arg=--lib, test set filtered to 4650 pmat801 chat_template_wiring thinking_on_refusal chat_template_official format_chat. 18 mutants, 11 caught, 7 missed, 0 unviable, 0 timeout. grammar/tool.rs, chat_template_official.rs and mod_create_demo.rs are include!d and cargo-mutants did not enumerate them, so the parser and the tojson filter went unmutated." + } + } + ] + } + ], + "results": [ + { + "ruleId": "mutants_survivors_in_diff", + "level": "warning", + "message": { + "text": "7 of 18 in-diff mutants survived: try_gpu_backend -> None and Some(Default::default()) (openai_handlers.rs:1067), try_cached_backend -> None and Some(Default::default()) (openai_handlers.rs:1185), delete ! in format_chat_messages_official_thinking_tools (realize_handlers.rs:275), and format_chat_messages_for_state_thinking -> Ok(String::new()) and Ok(\"xyzzy\") (realize_handlers.rs:323). The test filter limited which tests could kill them, so some may be killed by the full lib suite. The realize_handlers.rs:275 survivor is the empty-tools guard of the #4650 fix." + }, + "properties": { + "grounding": "measured", + "command": [ + "cargo", + "mutants", + "-p", + "aprender-serve", + "--in-diff", + "", + "--jobs", + "1", + "--cargo-arg=--lib" + ], + "exit_code": 2, + "stdout_sha256": "4580a915a8c1438873474bf135d4f078c0c79913aff45be31cf2b23ba076ee35", + "failure_scenario": "deleting the ! at realize_handlers.rs:275 makes the filter keep empty tool lists and render an empty tools block, and no filtered test fails", + "precision_class": "advisory" + } + } + ] + }, + { + "tool": { + "driver": { + "name": "antigravity" + } + }, + "results": [ + { + "ruleId": "agy_memory_probe_failure_reads_zero_free", + "level": "note", + "message": { + "text": "antigravity: executor.memory_info().unwrap_or((0, 0)) turns a failed cudaMemGetInfo into 0 bytes free: KV sized against 0 (2048 floor) and the FP16 batched prefill judged not to fit. The #3715 eprintln prints the 0.0 GB figure, so it is not wholly silent." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/aprender-serve/src/gguf/cuda/mod.rs" + }, + "region": { + "startLine": 820 + } + } + } + ], + "properties": { + "grounding": "asserted", + "rationale": "Raised by antigravity as asserted. The primary read lines 820-847 and confirms the unwrap_or((0,0)) and the eprintln; severity is judgement.", + "failure_scenario": "a transient driver error at load leaves the model on a 2048 context and serial prefill with only a stderr line", + "precision_class": "advisory" + } + }, + { + "ruleId": "agy_fit_serving_need_single_slot", + "level": "note", + "message": { + "text": "antigravity: with KvLen::FitServing the fit check computes need for one slot while serving_kv_len divides memory across 1 + SERVE_BATCH_SLOTS. Not re-verified by the primary." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/aprender-serve/src/gguf/cuda/mod.rs" + }, + "region": { + "startLine": 837 + } + } + } + ], + "properties": { + "grounding": "asserted", + "rationale": "Raised by antigravity; the primary read line 837 but did not trace the batch-slot allocation.", + "failure_scenario": "the FP16 prefill cache is kept when the batch slots then no longer fit and the scheduler OOMs at allocation", + "precision_class": "advisory" + } + }, + { + "ruleId": "agy_session_kv_len_not_recomputed_after_serial", + "level": "note", + "message": { + "text": "antigravity: after the switch to serial prefill max_seq_len stays clamped by the batched-prefill reserve. Not re-verified by the primary." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/aprender-serve/src/gguf/cuda/mod.rs" + }, + "region": { + "startLine": 829 + } + } + } + ], + "properties": { + "grounding": "asserted", + "rationale": "Raised by antigravity; the primary saw max_seq_len fixed at 829-831 before the fit check and did not measure the effect.", + "failure_scenario": "Qwen3-8B on 24 GB serves a smaller context than the memory allows", + "precision_class": "advisory" + } + }, + { + "ruleId": "agy_fp16_message_on_forced_serial", + "level": "note", + "message": { + "text": "antigravity: the [#3413] FP16 batched prefill message prints even when BATCHED_PREFILL=0 forces the serial path. Log-only. Not re-verified by the primary." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/aprender-serve/src/gguf/cuda/mod.rs" + }, + "region": { + "startLine": 792 + } + } + } + ], + "properties": { + "grounding": "asserted", + "rationale": "Raised by antigravity (line 792); the primary did not read it.", + "failure_scenario": "an operator reads the log line and chases a serial slowdown in the wrong place", + "precision_class": "advisory" + } + } + ] + } + ] +} diff --git a/evidence/pr-review/4711/e95438e370fed728ba10b78eb07e088fe2bc8097/receipt.intoto.jsonl b/evidence/pr-review/4711/e95438e370fed728ba10b78eb07e088fe2bc8097/receipt.intoto.jsonl new file mode 100644 index 0000000000..dac321ac69 --- /dev/null +++ b/evidence/pr-review/4711/e95438e370fed728ba10b78eb07e088fe2bc8097/receipt.intoto.jsonl @@ -0,0 +1 @@ +{"_type":"https://in-toto.io/Statement/v1","subject":[{"name":"git+https://github.com/paiml/aprender","digest":{"sha1":"e95438e370fed728ba10b78eb07e088fe2bc8097"}}],"predicateType":"https://paiml.dev/attestations/pr-review/v2","predicate":{"skill_version":"2.1.0","attestation_level":"L1-self","pr":4711,"base_sha":"11f844a772f77647d7d4b6281acbb2ab5e0bd76b","head_sha":"e95438e370fed728ba10b78eb07e088fe2bc8097","author_actor":{"kind":"agent","id":"agent:claude-opus-5-5/aprender-w4711"},"reviewer_actor":{"kind":"agent","id":"agent:claude-sonnet-5-5/w4711-review"},"affected_crates":["aprender-serve","apr-cli"],"verdict":"DEGRADED","degraded_reason":"mutation ran on gx10 at 942e3f8fe6 only with a filtered test set and left 7 of 18 in-diff mutants surviving (reused unchanged: the only later code commits, 68b281950c and e95438e370, touch one test file that cargo-mutants does not mutate); the include!d files grammar/tool.rs, chat_template_official.rs and mod_create_demo.rs are invisible to cargo-mutants; no cargo test or device run was possible on this host, so the e95438e370 behaviour (gx10 GB10: device run 1 passed/0 ignored, no device rc 101) rests on the author gx10 logs and reading; the cuda-docs-reviewer agent could not be dispatched from this lane so nvidia-cuda-docs was queried inline once more (no authority found); origin/main could not be refreshed (fetch blocked), the local origin/main equals base; the agy tree was built from git ls-files of a clean worktree at HEAD because the lane hook refuses git archive; agy status SUCCESS with a reviewed schema-valid structured_output but its 4 findings were not all re-verified by the primary; affected_crates derived from file paths. No blocking class (B1-B6) fires.","consultations":{"pmat":{"status":"consulted","transport":"cli","transport_unavailable":["mcp: not attempted this run (cli answered)"],"index_commit":"e95438e370fed728ba10b78eb07e088fe2bc8097","index_is_ancestor":true,"index_worktree_dirty":false,"complexity_delta":[{"file":"crates/aprender-serve/src/api/openai_handlers.rs","function":"build_tool_calling_message","base_cyclomatic":3,"head_cyclomatic":4,"base_cognitive":6,"head_cognitive":7},{"file":"crates/aprender-serve/src/gguf/cuda/mod.rs","function":"build","base_cyclomatic":6,"head_cyclomatic":8,"base_cognitive":11,"head_cognitive":13},{"file":"crates/apr-cli/src/commands/serve/mod.rs","function":"run","base_cyclomatic":6,"head_cyclomatic":7,"base_cognitive":7,"head_cognitive":8},{"file":"crates/aprender-serve/src/api/mod_create_demo.rs","function":"to_grammar","base_cyclomatic":1,"head_cyclomatic":3,"base_cognitive":0,"head_cognitive":5},{"file":"crates/aprender-serve/src/api/mod_create_demo.rs","function":"extract_tool_parameters","base_cyclomatic":3,"head_cyclomatic":5,"base_cognitive":2,"head_cognitive":6}],"tdg_delta":[{"file":"crates/apr-cli/src/commands/serve/handler_gpu_completion.rs","base":"A-","head":"A-","base_total":89.79,"head_total":88.52},{"file":"crates/aprender-serve/src/grammar/tool.rs","base":null,"head":"A","base_total":null,"head_total":91.0},{"file":"crates/aprender-serve/src/gguf/cuda/mod.rs","base":null,"head":"A","base_total":null,"head_total":90.0}],"satd_introduced":[],"cache_hits":0,"analysis_note":"pmat analyze complexity, satd and tdg over head and base copies of the 17 changed non-test .rs files. 0 SATD on both. Complexity of try_cuda_backend falls 14/43 -> 9/20 and completions_inner 9/26 -> 8/21. Scores are per copy and context dependent; files without a base copy show base null. Reused from 942e3f8fe6: the only later code commit (68b281950c) changes one test file, a cfg-test fn signature and an attribute, no new branch. At e95438e370 the K1 test is no longer #[ignore] (the module is cfg(feature=cuda), so cuda-unit runs it by name and the earlier lane-coverage finding is resolved); it still panics on init failure.","duplication_hits":[{"needle":"args_of","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-contracts/src/ontology/extract/cli_surface.rs","line":209,"method":"lexical"},{"needle":"args_of","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-contracts/src/ontology/extract/cli_surface.rs","line":217,"method":"lexical"},{"needle":"args_of","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-contracts/src/ontology/extract/cli_surface.rs","line":232,"method":"lexical"},{"needle":"fp16_cache","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-serve/src/gpu/streaming_kv_streaming.rs","line":278,"method":"lexical"},{"needle":"fp16_cache","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-serve/src/gpu/streaming_kv_streaming.rs","line":281,"method":"lexical"},{"needle":"fp16_cache","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-serve/tests/gpu_coverage.rs","line":1552,"method":"lexical"},{"needle":"fp16_cache","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-serve/tests/gpu_coverage.rs","line":1554,"method":"lexical"},{"needle":"resolve_serve_max_seq_len","kind":"name","where":"HEAD","ref":"HEAD","path":"evidence/serve/3791/Q4K.md","line":23,"method":"lexical"},{"needle":"serve_kv_limit_k1.rs","kind":"filename","where":"branch","ref":"origin/devont/k1-revert-red","path":"crates/aprender-serve/src/api/tests/serve_kv_limit_k1.rs","line":0,"method":"lexical"},{"needle":"session_kv.rs","kind":"filename","where":"branch","ref":"origin/devont/k1-revert-red","path":"crates/aprender-serve/src/gguf/cuda/session_kv.rs","line":0,"method":"lexical"},{"needle":"session_kv.rs","kind":"filename","where":"branch","ref":"origin/ec/3715-chunked-prefill","path":"crates/aprender-serve/src/gguf/cuda/session_kv.rs","line":0,"method":"lexical"},{"needle":"serve_kv_limit_k1.rs","kind":"filename","where":"branch","ref":"origin/pr/4631","path":"crates/aprender-serve/src/api/tests/serve_kv_limit_k1.rs","line":0,"method":"lexical"},{"needle":"session_kv.rs","kind":"filename","where":"branch","ref":"origin/pr/4631","path":"crates/aprender-serve/src/gguf/cuda/session_kv.rs","line":0,"method":"lexical"},{"needle":"serve_kv_limit_k1.rs","kind":"filename","where":"branch","ref":"origin/verify/6a-4606","path":"crates/aprender-serve/src/api/tests/serve_kv_limit_k1.rs","line":0,"method":"lexical"},{"needle":"session_kv.rs","kind":"filename","where":"branch","ref":"origin/verify/6a-4606","path":"crates/aprender-serve/src/gguf/cuda/session_kv.rs","line":0,"method":"lexical"},{"needle":"PMAT-4636.yaml","kind":"filename","where":"branch","ref":"origin/verify/6a-4606","path":"docs/roadmaps/entries/PMAT-4636.yaml","line":0,"method":"lexical"}],"duplication_coverage":{"rust":"semantic","shell":"lexical","python":"lexical","config":"lexical","docs":"lexical","other":"lexical","sibling_branches":"lexical","merge_base_to_main":"lexical"},"duplication_horizon":["head=e95438e370fed728ba10b78eb07e088fe2bc8097","siblings=refs/remotes/origin/* unmerged into origin/main","merge_base_to_main=11f844a772f77647d7d4b6281acbb2ab5e0bd76b..refs/remotes/origin/main"],"horizon_branches_total":1406,"horizon_branches_scanned":1406,"merge_base_to_main_files":0,"symbols_searched":71,"queries":[{"q":"qwen xml tool call parser function parameter","command":"pmat query \"qwen xml tool call parser function parameter\" --limit 3","cwd":"worktree at HEAD","results":3,"note":"top hits are parse_qwen_xml and qwen_xml_arguments in this PR; no prior implementation surfaced"}],"duplication_note":"all 16 hits are the K1/session-KV files from the merged #4623 on sibling branches, generic fp16_cache and args_of names, or an evidence page; none is landed prior art for the tools fix (merge_base_to_main_files 0)."},"cuda":{"status":"consulted","trigger_reason":"S3.B path trigger fires on api/cuda_chat_backend.rs and the #4623 files under gguf/cuda/ (path contains cuda). The diff adds no device-behaviour claim: the extract-fn refactor moves host code, and #4623 carries its own queries. The cuda-docs-reviewer agent could not be dispatched from this lane, so the primary queried the nvidia-cuda-docs MCP inline once. Re-fired at 68b281950c: serve_kv_limit_k1.rs exercises OwnedQuantizedModelCuda. Re-fired at e95438e370 (same test file).","queries":[{"q":"CUDA context bound to calling thread when running GPU kernels from a spawned blocking thread; cuCtxSetCurrent","source_uri":"nvidia-cuda-docs MCP (search_cuda_docs)","result":"no-authority-found","claim_checked":"The refactor keeps dispatch_cuda_stream inside the same blocking task as before, so the thread-current CUDA context is unchanged. The query returned the threadMigration driver-API sample (cuCtxPushCurrent per host thread) and nothing on spawn_blocking, so no authority was found for or against the claim."},{"q":"cuInit / device count zero: CUDA_ERROR_NO_DEVICE handling when no CUDA device is present","source_uri":"nvidia-cuda-docs MCP (search_cuda_docs)","result":"no-authority-found","claim_checked":"68b281950c panics when OwnedQuantizedModelCuda::for_serving fails to initialise a device. The query returned the cudaDeviceInit sample (cuInit, cuDeviceGetCount, exit on zero devices) which shows zero devices is an error path in the driver API, but nothing on how the Rust wrapper surfaces it, so no authority was found for or against the panic path."}]},"crux":{"status":"consulted","surfaces":["POST /v1/chat/completions request field tools","POST /v1/chat/completions response message.tool_calls and finish_reason"],"contracts":["contracts/crux-C-11-v1.yaml","contracts/crux-C-03-v1.yaml"],"gap_effect":"none","crux_coverage":"covered","comparative_claims":[]},"mutation":{"status":"consulted","scope":"in-diff","attempted":18,"killed":11,"survivors":[{"mutant":"replace try_gpu_backend -> Option with None","file":"crates/aprender-serve/src/api/openai_handlers.rs","line":1067,"killed":false},{"mutant":"replace try_gpu_backend -> Option with Some(Default::default())","file":"crates/aprender-serve/src/api/openai_handlers.rs","line":1067,"killed":false},{"mutant":"replace try_cached_backend -> Option with None","file":"crates/aprender-serve/src/api/openai_handlers.rs","line":1185,"killed":false},{"mutant":"replace try_cached_backend -> Option with Some(Default::default())","file":"crates/aprender-serve/src/api/openai_handlers.rs","line":1185,"killed":false},{"mutant":"delete ! in format_chat_messages_official_thinking_tools","file":"crates/aprender-serve/src/api/realize_handlers.rs","line":275,"killed":false},{"mutant":"replace format_chat_messages_for_state_thinking with Ok(String::new())","file":"crates/aprender-serve/src/api/realize_handlers.rs","line":323,"killed":false},{"mutant":"replace format_chat_messages_for_state_thinking with Ok(\"xyzzy\".into())","file":"crates/aprender-serve/src/api/realize_handlers.rs","line":323,"killed":false}]},"antigravity":{"status":"consulted","attempted":1,"agy_version":"agy 1.2.17","binary_path":"/home/noah/.local/bin/agy","model_id":"gemini-3.1-pro-high","model_family":"google/gemini","exit_code":0,"duration_seconds":106.844678336,"agy_status":"SUCCESS","usage":{"input_tokens":94004,"output_tokens":11280,"thinking_tokens":8867,"cache_read_tokens":967539,"total_tokens":105284},"output_check":{"structured_output_present":true,"reviewed":true,"schema_valid":true},"schema_check":"check-jsonschema --schemafile .claude/skills/pr-review/agy-review-v1.schema.json : ok","reverified_by_primary":false,"divergence_note":"agy status SUCCESS, reviewed true, schema-valid, disposable tree from git ls-files of the clean worktree at e95438e370 (git archive refused by the lane hook), diff = merge-base..head. agy raised 4 asserted findings, all in gguf/cuda/mod.rs (#4623 code): memory_info unwrap_or((0,0)) (primary confirmed lines 820-847), FitServing single-slot need, unrecomputed max_seq_len after serial switch, and an FP16 log line printed on forced serial (not verified). agy again did not raise the Qwen XML preemption. primary_only = 9: complexity, tdg, satd, refactor equivalence, qwen xml preemption, parser edge cases, legacy template noise, crux, mutation survivors.","divergence":{"agreed":0,"agy_only":4,"primary_only":9,"contradicted":0},"findings":[{"id":"fit_check_need_ignores_serve_batched_slots","file":"crates/aprender-serve/src/gguf/cuda/mod.rs","grounding":"asserted"},{"id":"memory_probe_failure_reads_as_zero_free","file":"crates/aprender-serve/src/gguf/cuda/mod.rs","grounding":"asserted"},{"id":"kv_not_recomputed_after_serial_fallback","file":"crates/aprender-serve/src/gguf/cuda/mod.rs","grounding":"asserted"},{"id":"fp16_message_printed_on_forced_serial","file":"crates/aprender-serve/src/gguf/cuda/mod.rs","grounding":"asserted"}]}},"findings_ref":{"path":"findings.sarif","sha256":"bcd7c0cd8c046de871043c5d5241d6d2d0ce64b63774fba0b7a4694795f7f5b8"},"cost":{"input_tokens":337713,"output_tokens":42491,"wall_seconds":3600},"diff_patch_id":"5d9c974f51e4df0a62a096bd3f2b9236a506a611","diff_patch_id_algo":"git-patch-id-verbatim/pinned-diff-v1"}} diff --git a/evidence/pr-review/4711/f2338256ee6a1a0b796f08657bf5c5efff58154b/agy.json b/evidence/pr-review/4711/f2338256ee6a1a0b796f08657bf5c5efff58154b/agy.json new file mode 100644 index 0000000000..7905e24ad6 --- /dev/null +++ b/evidence/pr-review/4711/f2338256ee6a1a0b796f08657bf5c5efff58154b/agy.json @@ -0,0 +1 @@ +{"conversation_id":"c866519c-1f42-4827-a10f-f99b364bef99","status":"SUCCESS","response":"{\"findings\":[{\"failure_scenario\":\"An operator who passes --context-length 4096 explicitly is indistinguishable from the default, so the export is skipped and the GPU KV is sized from the model context (up to what fits in VRAM) instead of the 4096 asked for.\",\"file\":\"crates/apr-cli/src/commands/serve/mod.rs\",\"grounding\":\"asserted\",\"id\":\"PRREV-K1-EXPLICIT-4096\",\"line\":308,\"summary\":\"apr serve exports REALIZR_CONTEXT_LENGTH only when config.context_length != DEFAULT_CONTEXT_LENGTH (4096).\"},{\"failure_scenario\":\"A client sends a 4095-token prompt with max_tokens 256 to a server whose serving context is 4096; the request is not refused, decode overflows the KV at token 2, and the client receives a 500 that says the server broke.\",\"file\":\"crates/aprender-serve/src/api/gpu_completions_handler.rs\",\"grounding\":\"cited\",\"id\":\"PRREV-K1-MAXTOKENS-500\",\"line\":431,\"source\":\"evidence/pr-review/4630/8df8d6d0a7d9e13288f82924a2dd0a2860a28cc5/findings.sarif\",\"summary\":\"serve_context_refusal refuses only prompt_tokens \\u003e= serving_context and ignores max_tokens.\"},{\"failure_scenario\":\"A batch of prompts with one prompt over the serving context relies on the typed-error path through generate_gpu_resident rather than a pre-flight; a regression to a raw GPU fault would silently return 500 with no test catching it.\",\"file\":\"crates/aprender-serve/src/api/batch_processing.rs\",\"grounding\":\"cited\",\"id\":\"gpu_batch_completions_missing_preflight\",\"line\":355,\"source\":\"evidence/pr-review/4630/8df8d6d0a7d9e13288f82924a2dd0a2860a28cc5/findings.sarif\",\"summary\":\"`/v1/batch/completions` (gpu_batch_completions_handler) has no serve_context_refusal pre-flight.\"},{\"failure_scenario\":\"Requests to /v1/generate with prompts over the serving context lose the immediate 400 if the downstream typed-error path regresses to a GPU fault, returning 500.\",\"file\":\"crates/aprender-serve/src/api/batch_processing.rs\",\"grounding\":\"cited\",\"id\":\"try_cuda_generate_missing_preflight\",\"line\":456,\"source\":\"evidence/pr-review/4630/8df8d6d0a7d9e13288f82924a2dd0a2860a28cc5/findings.sarif\",\"summary\":\"try_cuda_generate (/v1/generate) lacks the serve_context_refusal pre-flight.\"},{\"failure_scenario\":\"Logprobs or perplexity requests over the serving context are not refused early; a regression of generate_gpu_resident_logprobs to a raw fault returns 500 instead of 400.\",\"file\":\"crates/aprender-serve/src/api/gpu_completions_handler.rs\",\"grounding\":\"cited\",\"id\":\"logprobs_missing_preflight\",\"line\":706,\"source\":\"evidence/pr-review/4630/8df8d6d0a7d9e13288f82924a2dd0a2860a28cc5/findings.sarif\",\"summary\":\"logprobs_handler and perplexity_handler lack the serve_context_refusal pre-flight.\"}],\"reviewed\":true,\"toolAction\":\"Reporting PR findings\",\"toolSummary\":\"Report findings\"}\nI have reviewed the pull request diff and identified the following findings, which have been reported according to the required schema:\n\n1. **`PRREV-K1-EXPLICIT-4096`**: `apr serve` exports `REALIZR_CONTEXT_LENGTH` only when `config.context_length != DEFAULT_CONTEXT_LENGTH` (4096). This means an explicit `--context-length 4096` will be indistinguishable from the default and the GPU KV will be sized up to what fits in VRAM instead of the requested 4096. (*asserted*)\n2. **`PRREV-K1-MAXTOKENS-500`**: `serve_context_refusal` checks only if `prompt_tokens \u003e= serving_context` but ignores `max_tokens`. Thus, if `prompt_tokens \u003c serving_context` but `prompt + max_tokens \u003e serving_context`, the batch scheduler will try to execute it, overflow the KV at decode, and return a 500 error instead of a 400. (*cited from `findings.sarif`*)\n3. **`gpu_batch_completions_missing_preflight`**: The `/v1/batch/completions` handler lacks a pre-flight `serve_context_refusal` check, relying on the typed-error path which could silently regress to a 500 fault. (*cited from `findings.sarif`*)\n4. **`try_cuda_generate_missing_preflight`**: The `/v1/generate` endpoint also lacks the pre-flight check, meaning an overflow would lead to a 500 instead of 400 if the downstream typed-error path regresses. (*cited from `findings.sarif`*)\n5. **`logprobs_missing_preflight`**: `logprobs_handler` and `perplexity_handler` lack the pre-flight check as well. (*cited from `findings.sarif`*)\n\nNo further tool calls are required.\n","duration_seconds":192.746743827,"num_turns":1,"structured_output":{"findings":[{"failure_scenario":"An operator who passes --context-length 4096 explicitly is indistinguishable from the default, so the export is skipped and the GPU KV is sized from the model context (up to what fits in VRAM) instead of the 4096 asked for.","file":"crates/apr-cli/src/commands/serve/mod.rs","grounding":"asserted","id":"PRREV-K1-EXPLICIT-4096","line":308,"summary":"apr serve exports REALIZR_CONTEXT_LENGTH only when config.context_length != DEFAULT_CONTEXT_LENGTH (4096)."},{"failure_scenario":"A client sends a 4095-token prompt with max_tokens 256 to a server whose serving context is 4096; the request is not refused, decode overflows the KV at token 2, and the client receives a 500 that says the server broke.","file":"crates/aprender-serve/src/api/gpu_completions_handler.rs","grounding":"cited","id":"PRREV-K1-MAXTOKENS-500","line":431,"source":"evidence/pr-review/4630/8df8d6d0a7d9e13288f82924a2dd0a2860a28cc5/findings.sarif","summary":"serve_context_refusal refuses only prompt_tokens \u003e= serving_context and ignores max_tokens."},{"failure_scenario":"A batch of prompts with one prompt over the serving context relies on the typed-error path through generate_gpu_resident rather than a pre-flight; a regression to a raw GPU fault would silently return 500 with no test catching it.","file":"crates/aprender-serve/src/api/batch_processing.rs","grounding":"cited","id":"gpu_batch_completions_missing_preflight","line":355,"source":"evidence/pr-review/4630/8df8d6d0a7d9e13288f82924a2dd0a2860a28cc5/findings.sarif","summary":"`/v1/batch/completions` (gpu_batch_completions_handler) has no serve_context_refusal pre-flight."},{"failure_scenario":"Requests to /v1/generate with prompts over the serving context lose the immediate 400 if the downstream typed-error path regresses to a GPU fault, returning 500.","file":"crates/aprender-serve/src/api/batch_processing.rs","grounding":"cited","id":"try_cuda_generate_missing_preflight","line":456,"source":"evidence/pr-review/4630/8df8d6d0a7d9e13288f82924a2dd0a2860a28cc5/findings.sarif","summary":"try_cuda_generate (/v1/generate) lacks the serve_context_refusal pre-flight."},{"failure_scenario":"Logprobs or perplexity requests over the serving context are not refused early; a regression of generate_gpu_resident_logprobs to a raw fault returns 500 instead of 400.","file":"crates/aprender-serve/src/api/gpu_completions_handler.rs","grounding":"cited","id":"logprobs_missing_preflight","line":706,"source":"evidence/pr-review/4630/8df8d6d0a7d9e13288f82924a2dd0a2860a28cc5/findings.sarif","summary":"logprobs_handler and perplexity_handler lack the serve_context_refusal pre-flight."}],"reviewed":true},"json_schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://paiml.dev/schemas/pr-review/agy-review-v1.json","title":"PR-REVIEW-SKILL-002 v2 §3.E — the structured output agy is asked for","$comment":"NOT under schemas/. That directory is VENDORED UPSTREAM artifacts only — two files, a SHA-256 manifest and a sources.json recording where each came from, enforced by scripts/check_vendored_schemas.sh and its own mutation guard. This schema is authored here, has no upstream, and would have no honest row in sources.json; putting it there would make a provenance record that records nothing. It is the skill's own output contract, so it lives with the skill.","type":"object","additionalProperties":false,"required":["reviewed","findings"],"properties":{"reviewed":{"type":"boolean","description":"false means agy declined or could not review. The caller records status: unreachable and verdict DEGRADED — never a run that found nothing."},"findings":{"type":"array","description":"May be empty. An empty array under reviewed:true is 'ran and found nothing', which is a different artifact from 'could not run' (§3.0).","items":{"type":"object","additionalProperties":false,"required":["id","grounding","summary","failure_scenario"],"properties":{"id":{"type":"string","minLength":1},"grounding":{"enum":["cited","measured","asserted"],"description":"§1's three marks and there is no fourth. A `measured` finding here was measured by AGY's process, not the primary reviewer's — the receipt records that in reverified_by_primary (§3.E.5)."},"summary":{"type":"string","minLength":1},"failure_scenario":{"type":"string","minLength":1,"description":"§4.2: a finding that cannot name the concrete failure it permits is a comment, not a finding."},"file":{"type":"string"},"line":{"type":"integer","minimum":1},"source":{"type":"string","description":"Required by the guard when grounding is `cited`: a citation with no source is an assertion wearing the mark of a citation (§1.1)."},"command":{"type":"array","items":{"type":"string"},"description":"argv, when grounding is `measured`. It records what AGY ran, and the primary reviewer does not re-run it (§3.E.5)."}}}}}},"usage":{"input_tokens":129958,"output_tokens":19946,"thinking_tokens":15828,"cache_read_tokens":1351793,"total_tokens":149904}} diff --git a/evidence/pr-review/4711/f2338256ee6a1a0b796f08657bf5c5efff58154b/findings.sarif b/evidence/pr-review/4711/f2338256ee6a1a0b796f08657bf5c5efff58154b/findings.sarif new file mode 100644 index 0000000000..2a4869a576 --- /dev/null +++ b/evidence/pr-review/4711/f2338256ee6a1a0b796f08657bf5c5efff58154b/findings.sarif @@ -0,0 +1,357 @@ +{ + "version": "2.1.0", + "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json", + "runs": [ + { + "tool": { + "driver": { + "name": "pmat" + } + }, + "results": [ + { + "ruleId": "complexity_delta", + "level": "note", + "message": { + "text": "pmat analyze complexity over head and base copies of the 17 changed non-test .rs files. Only five functions rise: build_tool_calling_message 3->5 (re-measured at f70cf5a8fa), to_grammar 1->3, extract_tool_parameters 3->5, gguf/cuda/mod.rs build 6->8 (from #4623), serve run 6->7; all under 10. The extract-fn refactor lowers try_cuda_backend from cyclomatic 14 / cognitive 43 to 9 / 20 and completions_inner from 9 / 26 to 8 / 21. At 0b6096d752 build_tool_calling_message measures 4/7 (cyclomatic/cognitive) vs base 3/6; the previous head was 5/8, so the fallback removal lowers it." + }, + "properties": { + "grounding": "measured", + "command": [ + "pmat", + "analyze", + "complexity", + "--format", + "json", + "--path", + "" + ], + "exit_code": 0, + "stdout_sha256": "d811c87edb1a71e8ad5a404cb618a58cae0cb7b704af4be14f5efed8f5f92017", + "failure_scenario": "a later branch in extract_tool_parameters or try_cuda_backend crosses cyclomatic 10", + "precision_class": "advisory" + } + }, + { + "ruleId": "tdg_delta", + "level": "note", + "message": { + "text": "pmat analyze tdg: no changed file drops a grade. handler_gpu_completion.rs 89.79 -> 88.52, still A-. grammar/tool.rs and gguf/cuda/mod.rs have no base copy in the 17-file set (A at head)." + }, + "properties": { + "grounding": "measured", + "command": [ + "pmat", + "analyze", + "tdg", + "--format", + "json", + "--path", + "" + ], + "exit_code": 0, + "stdout_sha256": "e22114fee548d3aaf38120f9fc90d606f151177cd5e987d43c616a0e0f671442", + "failure_scenario": "none: grade unchanged", + "precision_class": "advisory" + } + }, + { + "ruleId": "satd_none_introduced", + "level": "note", + "message": { + "text": "pmat analyze satd over the 17 head files and the base copies: 0 violations on both." + }, + "properties": { + "grounding": "measured", + "command": [ + "pmat", + "analyze", + "satd", + "--format", + "json", + "--path", + "" + ], + "exit_code": 0, + "stdout_sha256": "4c677a9e5f190ffaa95b108f941bf34ba9f30a27c1aea16cbfbad1ac4f325e28", + "failure_scenario": "a TODO added by this diff would be counted", + "precision_class": "advisory" + } + }, + { + "ruleId": "refactor_equivalence_try_cuda_backend", + "level": "note", + "message": { + "text": "Commit 26ccc7f932 extracts dispatch_cuda_stream, cuda_batch_collect, cuda_direct_collect and try_cuda_direct_completions. Read line by line: the 503 batch-queue-full path keeps record_admission_rejected, the 500 and generation_error_status paths are unchanged, timing_tx sends are unchanged, and chain order is unchanged. Two clones became moves. No behaviour change found." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/aprender-serve/src/api/cuda_chat_backend.rs" + }, + "region": { + "startLine": 1 + } + } + } + ], + "properties": { + "grounding": "asserted", + "rationale": "Read by the primary against the base copy; no cargo test could be run on this host, and the extracted paths are device paths.", + "failure_scenario": "a dropped record_admission_rejected would under-count 503s with no test failing", + "precision_class": "advisory" + } + }, + { + "ruleId": "qwen_xml_parser_edge_cases", + "level": "note", + "message": { + "text": "parse_qwen_xml finds or at end of text, and does not skip think blocks. A call quoted inside reasoning, a truncated final call, or a literal inside a string value parse as a call or cut the value short." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/aprender-serve/src/grammar/tool.rs" + }, + "region": { + "startLine": 296 + } + } + } + ], + "properties": { + "grounding": "asserted", + "rationale": "Read of parse_qwen_xml, qwen_xml_arguments and qwen_xml_value. The module is include!d so no mutation or unit run covers it from this host.", + "failure_scenario": "a value containing is truncated at that tag and the rest is dropped", + "precision_class": "advisory" + } + }, + { + "ruleId": "legacy_template_tools_warning_noise", + "level": "note", + "message": { + "text": "The legacy template path prints an eprintln! warning on every request that carries tools. Honest, but it is stderr noise per request and not a tracing event." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/aprender-serve/src/api/realize_handlers.rs" + }, + "region": { + "startLine": 287 + } + } + } + ], + "properties": { + "grounding": "asserted", + "rationale": "Read; not run.", + "failure_scenario": "a client sending tools on every turn fills the server log", + "precision_class": "advisory" + } + }, + { + "ruleId": "qwen_xml_final_no_fallback", + "level": "note", + "message": { + "text": "0b6096d752 (unchanged at f2338256ee) makes the QwenXml result FINAL when the text contains block outranks a later JSON call. Not run here (no cargo on this host); the RED-at-f70, GREEN-at-head gx10 result is the author claim." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/aprender-serve/src/api/openai_handlers.rs" + }, + "region": { + "startLine": 643 + } + } + } + ], + "properties": { + "grounding": "asserted", + "rationale": "Read git diff f70cf5a8fa..0b6096d752 (one file) and lines 640-660; no test executed by the reviewer.", + "failure_scenario": "model text that mentions --jobs 1 --cargo-arg=--lib, test set filtered to 4650 pmat801 chat_template_wiring thinking_on_refusal chat_template_official format_chat. 18 mutants, 11 caught, 7 missed, 0 unviable, 0 timeout. grammar/tool.rs, chat_template_official.rs and mod_create_demo.rs are include!d and cargo-mutants did not enumerate them, so the parser and the tojson filter went unmutated." + } + } + ] + } + ], + "results": [ + { + "ruleId": "mutants_survivors_in_diff", + "level": "warning", + "message": { + "text": "7 of 18 in-diff mutants survived: try_gpu_backend -> None and Some(Default::default()) (openai_handlers.rs:1067), try_cached_backend -> None and Some(Default::default()) (openai_handlers.rs:1185), delete ! in format_chat_messages_official_thinking_tools (realize_handlers.rs:275), and format_chat_messages_for_state_thinking -> Ok(String::new()) and Ok(\"xyzzy\") (realize_handlers.rs:323). The test filter limited which tests could kill them, so some may be killed by the full lib suite. The realize_handlers.rs:275 survivor is the empty-tools guard of the #4650 fix." + }, + "properties": { + "grounding": "measured", + "command": [ + "cargo", + "mutants", + "-p", + "aprender-serve", + "--in-diff", + "", + "--jobs", + "1", + "--cargo-arg=--lib" + ], + "exit_code": 2, + "stdout_sha256": "4580a915a8c1438873474bf135d4f078c0c79913aff45be31cf2b23ba076ee35", + "failure_scenario": "deleting the ! at realize_handlers.rs:275 makes the filter keep empty tool lists and render an empty tools block, and no filtered test fails", + "precision_class": "advisory" + } + } + ] + }, + { + "tool": { + "driver": { + "name": "antigravity" + } + }, + "results": [ + { + "ruleId": "agy_fit_serving_need_single_slot", + "level": "note", + "message": { + "text": "antigravity: with KvLen::FitServing the fit check computes need for one KV slot while serving provisions 1 + SERVE_BATCH_SLOTS. Not re-verified by the primary." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/aprender-serve/src/gguf/cuda/mod.rs" + }, + "region": { + "startLine": 837 + } + } + } + ], + "properties": { + "grounding": "asserted", + "rationale": "Raised by antigravity; the primary read line 837 but did not trace the batch-slot allocation.", + "failure_scenario": "the FP16 prefill cache is kept when the batch slots then no longer fit and the scheduler OOMs at allocation", + "precision_class": "advisory" + } + }, + { + "ruleId": "agy_session_kv_len_not_recomputed_after_serial", + "level": "note", + "message": { + "text": "antigravity: after the switch to serial prefill max_seq_len stays clamped by the batched-prefill reserve. Agy cites line 838; the primary saw max_seq_len fixed at 829-831. Not re-verified." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/aprender-serve/src/gguf/cuda/mod.rs" + }, + "region": { + "startLine": 829 + } + } + } + ], + "properties": { + "grounding": "asserted", + "rationale": "Raised by antigravity; the primary read the ordering and did not measure the effect.", + "failure_scenario": "Qwen3-8B on 24 GB serves a smaller context than the memory allows", + "precision_class": "advisory" + } + }, + { + "ruleId": "agy_memory_probe_failure_reads_zero_free", + "level": "note", + "message": { + "text": "antigravity: executor.memory_info().unwrap_or((0, 0)) reads a failed cudaMemGetInfo as 0 bytes free and silently forces serial prefill; the eprintln then prints the 0.0 GB figure." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/aprender-serve/src/gguf/cuda/mod.rs" + }, + "region": { + "startLine": 820 + } + } + } + ], + "properties": { + "grounding": "asserted", + "rationale": "Raised by antigravity; the primary read 820-847 and confirms the unwrap_or((0,0)) and the eprintln.", + "failure_scenario": "a transient driver error at load leaves a 2048 context and serial prefill with only a stderr line", + "precision_class": "advisory" + } + } + ] + } + ] +} diff --git a/evidence/pr-review/4711/f2338256ee6a1a0b796f08657bf5c5efff58154b/receipt.intoto.jsonl b/evidence/pr-review/4711/f2338256ee6a1a0b796f08657bf5c5efff58154b/receipt.intoto.jsonl new file mode 100644 index 0000000000..7222288b24 --- /dev/null +++ b/evidence/pr-review/4711/f2338256ee6a1a0b796f08657bf5c5efff58154b/receipt.intoto.jsonl @@ -0,0 +1 @@ +{"_type":"https://in-toto.io/Statement/v1","subject":[{"name":"git+https://github.com/paiml/aprender","digest":{"sha1":"f2338256ee6a1a0b796f08657bf5c5efff58154b"}}],"predicateType":"https://paiml.dev/attestations/pr-review/v2","predicate":{"skill_version":"2.1.0","attestation_level":"L1-self","pr":4711,"base_sha":"11f844a772f77647d7d4b6281acbb2ab5e0bd76b","head_sha":"f2338256ee6a1a0b796f08657bf5c5efff58154b","author_actor":{"kind":"agent","id":"agent:claude-opus-5-5/aprender-w4711"},"reviewer_actor":{"kind":"agent","id":"agent:claude-sonnet-5-5/w4711-review"},"affected_crates":["aprender-serve","apr-cli"],"verdict":"DEGRADED","degraded_reason":"mutation ran on gx10 at 942e3f8fe6 only with a filtered test set and left 7 of 18 in-diff mutants surviving; it was NOT re-run for f2338256ee, so the survivor delete ! at realize_handlers.rs:275 stays listed as measured; the new test request_tools_reach_the_ggufs_own_template_4650 kills it only per the author gx10 proof (GREEN at head, RED with ! deleted), not re-run here; it was also NOT re-run for 0b6096d752, which makes the QwenXml result final (drops the JSON fallback) of build_tool_calling_message in openai_handlers.rs (new code unmutated); the include!d files grammar/tool.rs, chat_template_official.rs and mod_create_demo.rs are invisible to cargo-mutants; no cargo test or device run was possible on this host, so the new qwen35_undeclared_xml_call_does_not_promote_inner_json_4650 test (RED at f70cf5a8fa, GREEN at head per gx10 author claim) and the K1 gx10 results rest on the author logs and reading; the cuda-docs-reviewer agent could not be dispatched from this lane so nvidia-cuda-docs was queried inline once at 68b281950c (no authority found) and not re-queried; origin/main could not be refreshed (fetch blocked), local origin/main equals base; the agy tree was built from git ls-files of a clean worktree because the lane hook refuses git archive; agy status SUCCESS, reviewed schema-valid output; of its 5 findings 4 are cited from the 4630 review evidence (source files exist, not re-verified against code beyond reading serve_context_refusal and the K1 export guard) and 1 is asserted; none addresses the parser change or the new test; affected_crates derived from file paths. No blocking class (B1-B6) fires.","consultations":{"pmat":{"status":"consulted","transport":"cli","transport_unavailable":["mcp: not attempted this run (cli answered)"],"index_commit":"f2338256ee6a1a0b796f08657bf5c5efff58154b","index_is_ancestor":true,"index_worktree_dirty":false,"complexity_delta":[{"file":"crates/aprender-serve/src/api/openai_handlers.rs","function":"build_tool_calling_message","base_cyclomatic":3,"head_cyclomatic":4,"base_cognitive":6,"head_cognitive":7},{"file":"crates/aprender-serve/src/gguf/cuda/mod.rs","function":"build","base_cyclomatic":6,"head_cyclomatic":8,"base_cognitive":11,"head_cognitive":13},{"file":"crates/apr-cli/src/commands/serve/mod.rs","function":"run","base_cyclomatic":6,"head_cyclomatic":7,"base_cognitive":7,"head_cognitive":8},{"file":"crates/aprender-serve/src/api/mod_create_demo.rs","function":"to_grammar","base_cyclomatic":1,"head_cyclomatic":3,"base_cognitive":0,"head_cognitive":5},{"file":"crates/aprender-serve/src/api/mod_create_demo.rs","function":"extract_tool_parameters","base_cyclomatic":3,"head_cyclomatic":5,"base_cognitive":2,"head_cognitive":6}],"tdg_delta":[{"file":"crates/apr-cli/src/commands/serve/handler_gpu_completion.rs","base":"A-","head":"A-","base_total":89.79,"head_total":88.52},{"file":"crates/aprender-serve/src/grammar/tool.rs","base":null,"head":"A","base_total":null,"head_total":91.0},{"file":"crates/aprender-serve/src/gguf/cuda/mod.rs","base":null,"head":"A","base_total":null,"head_total":90.0}],"satd_introduced":[],"cache_hits":0,"analysis_note":"pmat analyze complexity, satd and tdg over head and base copies of the 17 changed non-test .rs files. 0 SATD on both. Complexity of try_cuda_backend falls 14/43 -> 9/20 and completions_inner 9/26 -> 8/21. Scores are per copy and context dependent; files without a base copy show base null. Reused from 942e3f8fe6: the only later code commit (68b281950c) changes one test file, a cfg-test fn signature and an attribute, no new branch. At e95438e370 the K1 test is no longer #[ignore] (the module is cfg(feature=cuda), so cuda-unit runs it by name and the earlier lane-coverage finding is resolved); it still panics on init failure. At f70cf5a8fa build_tool_calling_message was re-measured with pmat analyze complexity on base and head copies: cyclomatic 3->5, cognitive 6->8. Other deltas reused from 942e3f8fe6. At 0b6096d752 only openai_handlers.rs changed (build_tool_calling_message re-measured 4/7 vs base 3/6, from head and base copies); other measurements reused from f70cf5a8fa. f2338256ee adds one test only (request_tools_reach_the_ggufs_own_template_4650, include!d realize_handlers_official_3990_tests.rs): no production code, no complexity or SATD change; measurements reused from 0b6096d752.","duplication_hits":[{"needle":"args_of","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-contracts/src/ontology/extract/cli_surface.rs","line":209,"method":"lexical"},{"needle":"args_of","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-contracts/src/ontology/extract/cli_surface.rs","line":217,"method":"lexical"},{"needle":"args_of","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-contracts/src/ontology/extract/cli_surface.rs","line":232,"method":"lexical"},{"needle":"fp16_cache","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-serve/src/gpu/streaming_kv_streaming.rs","line":278,"method":"lexical"},{"needle":"fp16_cache","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-serve/src/gpu/streaming_kv_streaming.rs","line":281,"method":"lexical"},{"needle":"fp16_cache","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-serve/tests/gpu_coverage.rs","line":1552,"method":"lexical"},{"needle":"fp16_cache","kind":"name","where":"HEAD","ref":"HEAD","path":"crates/aprender-serve/tests/gpu_coverage.rs","line":1554,"method":"lexical"},{"needle":"resolve_serve_max_seq_len","kind":"name","where":"HEAD","ref":"HEAD","path":"evidence/serve/3791/Q4K.md","line":23,"method":"lexical"},{"needle":"serve_kv_limit_k1.rs","kind":"filename","where":"branch","ref":"origin/devont/k1-revert-red","path":"crates/aprender-serve/src/api/tests/serve_kv_limit_k1.rs","line":0,"method":"lexical"},{"needle":"session_kv.rs","kind":"filename","where":"branch","ref":"origin/devont/k1-revert-red","path":"crates/aprender-serve/src/gguf/cuda/session_kv.rs","line":0,"method":"lexical"},{"needle":"session_kv.rs","kind":"filename","where":"branch","ref":"origin/ec/3715-chunked-prefill","path":"crates/aprender-serve/src/gguf/cuda/session_kv.rs","line":0,"method":"lexical"},{"needle":"serve_kv_limit_k1.rs","kind":"filename","where":"branch","ref":"origin/pr/4631","path":"crates/aprender-serve/src/api/tests/serve_kv_limit_k1.rs","line":0,"method":"lexical"},{"needle":"session_kv.rs","kind":"filename","where":"branch","ref":"origin/pr/4631","path":"crates/aprender-serve/src/gguf/cuda/session_kv.rs","line":0,"method":"lexical"},{"needle":"serve_kv_limit_k1.rs","kind":"filename","where":"branch","ref":"origin/verify/6a-4606","path":"crates/aprender-serve/src/api/tests/serve_kv_limit_k1.rs","line":0,"method":"lexical"},{"needle":"session_kv.rs","kind":"filename","where":"branch","ref":"origin/verify/6a-4606","path":"crates/aprender-serve/src/gguf/cuda/session_kv.rs","line":0,"method":"lexical"},{"needle":"PMAT-4636.yaml","kind":"filename","where":"branch","ref":"origin/verify/6a-4606","path":"docs/roadmaps/entries/PMAT-4636.yaml","line":0,"method":"lexical"}],"duplication_coverage":{"rust":"semantic","shell":"lexical","python":"lexical","config":"lexical","docs":"lexical","other":"lexical","sibling_branches":"lexical","merge_base_to_main":"lexical"},"duplication_horizon":["head=f2338256ee6a1a0b796f08657bf5c5efff58154b","siblings=refs/remotes/origin/* unmerged into origin/main","merge_base_to_main=11f844a772f77647d7d4b6281acbb2ab5e0bd76b..refs/remotes/origin/main"],"horizon_branches_total":1406,"horizon_branches_scanned":1406,"merge_base_to_main_files":0,"symbols_searched":71,"queries":[{"q":"qwen xml tool call parser function parameter","command":"pmat query \"qwen xml tool call parser function parameter\" --limit 3","cwd":"worktree at HEAD","results":3,"note":"top hits are parse_qwen_xml and qwen_xml_arguments in this PR; no prior implementation surfaced"}],"duplication_note":"all 16 hits are the K1/session-KV files from the merged #4623 on sibling branches, generic fp16_cache and args_of names, or an evidence page; none is landed prior art for the tools fix (merge_base_to_main_files 0)."},"cuda":{"status":"consulted","trigger_reason":"S3.B path trigger fires on api/cuda_chat_backend.rs and the #4623 files under gguf/cuda/ (path contains cuda). The diff adds no device-behaviour claim: the extract-fn refactor moves host code, and #4623 carries its own queries. The cuda-docs-reviewer agent could not be dispatched from this lane, so the primary queried the nvidia-cuda-docs MCP inline once. Re-fired at 68b281950c: serve_kv_limit_k1.rs exercises OwnedQuantizedModelCuda. Re-fired at e95438e370 (same test file). Not re-queried at f70cf5a8fa: the new commit is host-side parser code in openai_handlers.rs.","queries":[{"q":"CUDA context bound to calling thread when running GPU kernels from a spawned blocking thread; cuCtxSetCurrent","source_uri":"nvidia-cuda-docs MCP (search_cuda_docs)","result":"no-authority-found","claim_checked":"The refactor keeps dispatch_cuda_stream inside the same blocking task as before, so the thread-current CUDA context is unchanged. The query returned the threadMigration driver-API sample (cuCtxPushCurrent per host thread) and nothing on spawn_blocking, so no authority was found for or against the claim."},{"q":"cuInit / device count zero: CUDA_ERROR_NO_DEVICE handling when no CUDA device is present","source_uri":"nvidia-cuda-docs MCP (search_cuda_docs)","result":"no-authority-found","claim_checked":"68b281950c panics when OwnedQuantizedModelCuda::for_serving fails to initialise a device. The query returned the cudaDeviceInit sample (cuInit, cuDeviceGetCount, exit on zero devices) which shows zero devices is an error path in the driver API, but nothing on how the Rust wrapper surfaces it, so no authority was found for or against the panic path."}]},"crux":{"status":"consulted","surfaces":["POST /v1/chat/completions request field tools","POST /v1/chat/completions response message.tool_calls and finish_reason"],"contracts":["contracts/crux-C-11-v1.yaml","contracts/crux-C-03-v1.yaml"],"gap_effect":"none","crux_coverage":"covered","comparative_claims":[]},"mutation":{"status":"consulted","scope":"in-diff","attempted":18,"killed":11,"survivors":[{"mutant":"replace try_gpu_backend -> Option with None","file":"crates/aprender-serve/src/api/openai_handlers.rs","line":1067,"killed":false},{"mutant":"replace try_gpu_backend -> Option with Some(Default::default())","file":"crates/aprender-serve/src/api/openai_handlers.rs","line":1067,"killed":false},{"mutant":"replace try_cached_backend -> Option with None","file":"crates/aprender-serve/src/api/openai_handlers.rs","line":1185,"killed":false},{"mutant":"replace try_cached_backend -> Option with Some(Default::default())","file":"crates/aprender-serve/src/api/openai_handlers.rs","line":1185,"killed":false},{"mutant":"delete ! in format_chat_messages_official_thinking_tools","file":"crates/aprender-serve/src/api/realize_handlers.rs","line":275,"killed":false},{"mutant":"replace format_chat_messages_for_state_thinking with Ok(String::new())","file":"crates/aprender-serve/src/api/realize_handlers.rs","line":323,"killed":false},{"mutant":"replace format_chat_messages_for_state_thinking with Ok(\"xyzzy\".into())","file":"crates/aprender-serve/src/api/realize_handlers.rs","line":323,"killed":false}]},"antigravity":{"status":"consulted","attempted":1,"agy_version":"agy 1.2.17","binary_path":"/home/noah/.local/bin/agy","model_id":"gemini-3.1-pro-high","model_family":"google/gemini","exit_code":0,"duration_seconds":192.746743827,"agy_status":"SUCCESS","usage":{"input_tokens":129958,"output_tokens":19946,"thinking_tokens":15828,"cache_read_tokens":1351793,"total_tokens":149904},"output_check":{"structured_output_present":true,"reviewed":true,"schema_valid":true},"schema_check":"check-jsonschema --schemafile .claude/skills/pr-review/agy-review-v1.schema.json : ok","reverified_by_primary":false,"divergence_note":"agy status SUCCESS, exit 0, reviewed schema-valid structured_output. Disposable tree from git ls-files of the clean worktree at f2338256ee, diff = merge-base..head excluding evidence/pr-review/4711. agy raised 5 findings: 1 asserted (apr serve cannot tell an explicit --context-length 4096 from the default, serve/mod.rs:308, read and confirmed) and 4 cited from evidence/pr-review/4630/8df8d6d0.../findings.sarif (max_tokens ignored by serve_context_refusal; missing pre-flights in batch/completions, /v1/generate and logprobs/perplexity); the cited source entries exist. All advisory. agy did not address the parser change or the new test.","divergence":{"agreed":0,"agy_only":5,"primary_only":9,"contradicted":0},"findings":[{"id":"PRREV-K1-EXPLICIT-4096","file":"crates/apr-cli/src/commands/serve/mod.rs","grounding":"asserted"},{"id":"PRREV-K1-MAXTOKENS-500","file":"crates/aprender-serve/src/api/gpu_completions_handler.rs","grounding":"cited","source":"evidence/pr-review/4630/8df8d6d0a7d9e13288f82924a2dd0a2860a28cc5/findings.sarif"},{"id":"gpu_batch_completions_missing_preflight","file":"crates/aprender-serve/src/api/batch_processing.rs","grounding":"cited","source":"evidence/pr-review/4630/8df8d6d0a7d9e13288f82924a2dd0a2860a28cc5/findings.sarif"},{"id":"try_cuda_generate_missing_preflight","file":"crates/aprender-serve/src/api/batch_processing.rs","grounding":"cited","source":"evidence/pr-review/4630/8df8d6d0a7d9e13288f82924a2dd0a2860a28cc5/findings.sarif"},{"id":"logprobs_missing_preflight","file":"crates/aprender-serve/src/api/gpu_completions_handler.rs","grounding":"cited","source":"evidence/pr-review/4630/8df8d6d0a7d9e13288f82924a2dd0a2860a28cc5/findings.sarif"}]}},"findings_ref":{"path":"findings.sarif","sha256":"8ddcedff0cc876a851d61080f50ab18ff37ef6f25000a40449580ef0c02f746a"},"cost":{"input_tokens":337713,"output_tokens":42491,"wall_seconds":3600},"diff_patch_id":"da2645712fb6dfc02bb747ce1362657d2f81d990","diff_patch_id_algo":"git-patch-id-verbatim/pinned-diff-v1"}}