Skip to content

Commit 87bde0a

Browse files
committed
review fixes
1 parent 2ed2b70 commit 87bde0a

12 files changed

Lines changed: 125 additions & 71 deletions

File tree

fixtures/mcp_calc_server/src/main.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ use std::io::{self, BufRead, Write};
66

77
#[derive(Deserialize)]
88
struct JsonRpcRequest {
9-
#[allow(dead_code)]
10-
jsonrpc: String,
119
id: Option<u64>,
1210
method: String,
1311
params: Option<Value>,

src/agent.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,13 @@ pub fn run_turn(ctx: &Context, user_input: &str, messages: &mut Vec<Value>) -> R
262262
let choice = &response.choices[0];
263263
let msg = &choice.message;
264264

265+
// Warn if response was truncated due to length limit
266+
if choice.finish_reason.as_deref() == Some("length") {
267+
eprintln!(
268+
"⚠️ Response truncated (max tokens reached). Consider increasing max_tokens or using /compact."
269+
);
270+
}
271+
265272
if let Some(content) = &msg.content {
266273
if !content.is_empty() {
267274
println!("{}", content);

src/commands.rs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,16 @@ impl CommandIndex {
9797
}
9898
}
9999

100-
fn load_command(&self, path: &Path, name: &str, source: CommandSource) -> Result<Command> {
100+
fn load_command(&mut self, path: &Path, name: &str, source: CommandSource) -> Result<Command> {
101101
let content = std::fs::read_to_string(path)?;
102102

103103
// Parse optional YAML frontmatter
104-
let (meta, content) = parse_frontmatter(&content);
104+
let (meta, content, warning) = parse_frontmatter(&content);
105+
106+
// Record warning but still load the command
107+
if let Some(warn) = warning {
108+
self.errors.push((path.to_path_buf(), warn));
109+
}
105110

106111
Ok(Command {
107112
name: name.to_string(),
@@ -130,11 +135,12 @@ impl CommandIndex {
130135
}
131136

132137
/// Parse optional YAML frontmatter from markdown content
133-
fn parse_frontmatter(content: &str) -> (CommandMeta, String) {
138+
/// Returns (metadata, body, optional_warning)
139+
fn parse_frontmatter(content: &str) -> (CommandMeta, String, Option<String>) {
134140
let trimmed = content.trim_start();
135141

136142
if !trimmed.starts_with("---") {
137-
return (CommandMeta::default(), content.to_string());
143+
return (CommandMeta::default(), content.to_string(), None);
138144
}
139145

140146
// Find the closing ---
@@ -143,11 +149,15 @@ fn parse_frontmatter(content: &str) -> (CommandMeta, String) {
143149
let rest = &trimmed[3 + end_pos + 4..].trim_start();
144150

145151
match serde_yaml::from_str(yaml_content) {
146-
Ok(meta) => (meta, rest.to_string()),
147-
Err(_) => (CommandMeta::default(), content.to_string()),
152+
Ok(meta) => (meta, rest.to_string(), None),
153+
Err(e) => (
154+
CommandMeta::default(),
155+
content.to_string(),
156+
Some(format!("invalid YAML frontmatter: {}", e)),
157+
),
148158
}
149159
} else {
150-
(CommandMeta::default(), content.to_string())
160+
(CommandMeta::default(), content.to_string(), None)
151161
}
152162
}
153163

@@ -158,9 +168,10 @@ mod tests {
158168
#[test]
159169
fn test_parse_frontmatter_no_frontmatter() {
160170
let content = "Just some content";
161-
let (meta, body) = parse_frontmatter(content);
171+
let (meta, body, warning) = parse_frontmatter(content);
162172
assert!(meta.description.is_none());
163173
assert_eq!(body, "Just some content");
174+
assert!(warning.is_none());
164175
}
165176

166177
#[test]
@@ -173,13 +184,14 @@ allowed_tools:
173184
---
174185
175186
The actual command content"#;
176-
let (meta, body) = parse_frontmatter(content);
187+
let (meta, body, warning) = parse_frontmatter(content);
177188
assert_eq!(meta.description, Some("A test command".to_string()));
178189
assert_eq!(
179190
meta.allowed_tools,
180191
Some(vec!["Read".to_string(), "Grep".to_string()])
181192
);
182193
assert_eq!(body, "The actual command content");
194+
assert!(warning.is_none());
183195
}
184196

185197
#[test]

src/config.rs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -620,13 +620,25 @@ impl Config {
620620
}
621621
}
622622

623-
// Validate MCP server configs
623+
// Validate MCP server configs based on transport type
624624
for (name, server) in &self.mcp.servers {
625-
if server.command.is_empty() {
626-
errors.push(ValidationError {
627-
field: format!("mcp.servers.{}.command", name),
628-
message: "Command must not be empty".to_string(),
629-
});
625+
match server.transport {
626+
McpTransport::Stdio => {
627+
if server.command.is_empty() {
628+
errors.push(ValidationError {
629+
field: format!("mcp.servers.{}.command", name),
630+
message: "Command required for stdio transport".to_string(),
631+
});
632+
}
633+
}
634+
McpTransport::Http | McpTransport::Sse => {
635+
if server.url.is_none() {
636+
errors.push(ValidationError {
637+
field: format!("mcp.servers.{}.url", name),
638+
message: "URL required for http/sse transport".to_string(),
639+
});
640+
}
641+
}
630642
}
631643
}
632644

@@ -716,6 +728,7 @@ mod tests {
716728
let errors = config.validate().unwrap_err();
717729
assert_eq!(errors.len(), 1);
718730
assert!(errors[0].field.contains("auto_compact_threshold"));
731+
assert!(errors[0].message.contains("between 0.0 and 1.0"));
719732
}
720733

721734
#[test]

src/cost.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ impl PricingTable {
261261
ModelPricing::new(15.00, 75.00),
262262
);
263263

264-
// Venice / free tier models (approximate as free)
264+
// Venice.ai models - free tier default, override via [pricing] config if needed
265265
models.insert(
266266
"qwen3-235b-a22b-instruct-2507".to_string(),
267267
ModelPricing::new(0.00, 0.00),

src/llm.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,6 @@ pub struct Usage {
1919
pub prompt_tokens: u64,
2020
#[serde(default)]
2121
pub completion_tokens: u64,
22-
/// Total tokens from API (may be redundant with prompt_tokens + completion_tokens)
23-
#[serde(default)]
24-
#[allow(dead_code)]
25-
pub total_tokens: u64,
2622
}
2723

2824
#[derive(Debug, Deserialize)]
@@ -35,7 +31,6 @@ pub struct ChatResponse {
3531
#[derive(Debug, Deserialize)]
3632
pub struct Choice {
3733
pub message: Message,
38-
#[allow(dead_code)]
3934
pub finish_reason: Option<String>,
4035
}
4136

src/mcp/client.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,6 @@ struct JsonRpcRequest {
2020
/// JSON-RPC response structure
2121
#[derive(Deserialize)]
2222
struct JsonRpcResponse {
23-
#[allow(dead_code)]
24-
jsonrpc: String,
25-
#[allow(dead_code)]
26-
id: Option<u64>,
2723
result: Option<Value>,
2824
error: Option<JsonRpcError>,
2925
}
@@ -92,12 +88,14 @@ impl McpClient {
9288
let result = self.call("initialize", Some(params))?;
9389

9490
// Send initialized notification (no response expected)
95-
// For HTTP/SSE this is a fire-and-forget, result is ignored
91+
// For HTTP/SSE this is a fire-and-forget, but log errors for debugging
9692
let notification = json!({
9793
"jsonrpc": "2.0",
9894
"method": "notifications/initialized"
9995
});
100-
let _ = self.transport.send(&notification);
96+
if let Err(e) = self.transport.send(&notification) {
97+
eprintln!("MCP: Failed to send initialized notification: {}", e);
98+
}
10199

102100
Ok(result)
103101
}

src/mcp/transport.rs

Lines changed: 58 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -225,9 +225,15 @@ impl SseTransport {
225225

226226
match resp {
227227
Ok(r) => {
228-
// For simple SSE implementations, the response comes back directly
229-
let body: Value = r.into_json()?;
230-
Ok(body)
228+
// Check if response is SSE stream or direct JSON
229+
let content_type = r.header("content-type").unwrap_or("").to_lowercase();
230+
if content_type.contains("text/event-stream") {
231+
self.parse_sse_response(request_id, r)
232+
} else {
233+
// For simple implementations, the response comes back as JSON directly
234+
let body: Value = r.into_json()?;
235+
Ok(body)
236+
}
231237
}
232238
Err(ureq::Error::Status(code, resp)) => {
233239
// Try to get SSE response from the event endpoint
@@ -238,48 +244,64 @@ impl SseTransport {
238244
}
239245
}
240246

247+
/// Parse SSE event stream from a response
248+
fn parse_sse_response(&self, request_id: Option<u64>, resp: ureq::Response) -> Result<Value> {
249+
let mut reader = BufReader::new(resp.into_reader());
250+
let mut line = String::new();
251+
let mut data = String::new();
252+
let mut events_read = 0;
253+
const MAX_EVENTS: usize = 1000; // Prevent infinite loops
254+
255+
loop {
256+
line.clear();
257+
match reader.read_line(&mut line) {
258+
Ok(0) => break, // EOF
259+
Ok(_) => {
260+
let line = line.trim();
261+
if let Some(stripped) = line.strip_prefix("data:") {
262+
data = stripped.trim().to_string();
263+
} else if line.is_empty() && !data.is_empty() {
264+
// End of event, parse the data
265+
events_read += 1;
266+
if events_read > MAX_EVENTS {
267+
return Err(anyhow::anyhow!(
268+
"SSE stream exceeded {} events without matching response",
269+
MAX_EVENTS
270+
));
271+
}
272+
if let Ok(value) = serde_json::from_str::<Value>(&data) {
273+
// Check if this is the response we're waiting for
274+
if let Some(id) = request_id {
275+
if value.get("id").and_then(|v| v.as_u64()) == Some(id) {
276+
return Ok(value);
277+
}
278+
} else {
279+
return Ok(value);
280+
}
281+
}
282+
data.clear();
283+
}
284+
}
285+
Err(e) => return Err(anyhow::anyhow!("SSE read error: {}", e)),
286+
}
287+
}
288+
289+
Err(anyhow::anyhow!(
290+
"SSE stream ended without matching response"
291+
))
292+
}
293+
241294
fn try_sse_fallback(
242295
&self,
243296
request_id: Option<u64>,
244297
http_code: u16,
245298
resp: ureq::Response,
246299
) -> Result<Value> {
247-
// If the POST returned a redirect or the response is chunked,
248-
// try to read it as SSE
300+
// If the POST returned an error status, check if it's an SSE stream
249301
let content_type = resp.header("content-type").unwrap_or("").to_lowercase();
250302

251303
if content_type.contains("text/event-stream") {
252-
// Parse SSE events
253-
let mut reader = BufReader::new(resp.into_reader());
254-
let mut line = String::new();
255-
let mut data = String::new();
256-
257-
loop {
258-
line.clear();
259-
match reader.read_line(&mut line) {
260-
Ok(0) => break, // EOF
261-
Ok(_) => {
262-
let line = line.trim();
263-
if let Some(stripped) = line.strip_prefix("data:") {
264-
data = stripped.trim().to_string();
265-
} else if line.is_empty() && !data.is_empty() {
266-
// End of event, parse the data
267-
if let Ok(value) = serde_json::from_str::<Value>(&data) {
268-
// Check if this is the response we're waiting for
269-
if let Some(id) = request_id {
270-
if value.get("id").and_then(|v| v.as_u64()) == Some(id) {
271-
return Ok(value);
272-
}
273-
} else {
274-
return Ok(value);
275-
}
276-
}
277-
data.clear();
278-
}
279-
}
280-
Err(e) => return Err(anyhow::anyhow!("SSE read error: {}", e)),
281-
}
282-
}
304+
return self.parse_sse_response(request_id, resp);
283305
}
284306

285307
Err(anyhow::anyhow!(

src/skillpacks/activation.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ pub struct SkillActivation {
1010
pub name: String,
1111
pub description: String,
1212
pub allowed_tools: Option<Vec<String>>,
13-
#[allow(dead_code)]
14-
pub instructions: String,
1513
}
1614

1715
/// Manages the set of active skills
@@ -44,7 +42,6 @@ impl ActiveSkills {
4442
name: pack.name.clone(),
4543
description: pack.description.clone(),
4644
allowed_tools: pack.allowed_tools.clone(),
47-
instructions: pack.instructions.clone(),
4845
};
4946

5047
self.active.insert(pack.name.clone(), pack);

src/subagent.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,16 @@ pub fn run_subagent(
324324
let choice = &response.choices[0];
325325
let msg = &choice.message;
326326

327+
// Warn if response was truncated due to length limit
328+
if choice.finish_reason.as_deref() == Some("length") {
329+
trace(
330+
ctx,
331+
agent_name,
332+
"WARN",
333+
"Response truncated (max tokens reached)",
334+
);
335+
}
336+
327337
// Collect assistant text
328338
if let Some(content) = &msg.content {
329339
if !content.is_empty() {

0 commit comments

Comments
 (0)