Skip to content

Commit 0c3c733

Browse files
committed
tighten comment hygiene and extract magic-literal consts
1 parent 77a1107 commit 0c3c733

15 files changed

Lines changed: 113 additions & 78 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ Gitignored scratchpad for helper files the user asks to be created there — typ
436436
- `cargo clippy --all-targets -- -D warnings`
437437
- `cargo test --all`
438438
- Before finishing, review the change for bugs and corner cases.
439+
- After each final modification, suggest a clear one-line commit message.
439440

440441
---
441442

src/api/anthropic.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@ use reqwest::header::{HeaderMap, HeaderValue};
66
use std::sync::Arc;
77
use std::sync::atomic::{AtomicBool, Ordering};
88

9-
const API_BASE: &str = "https://api.anthropic.com/v1";
10-
const API_VERSION: &str = "2023-06-01";
11-
/// Header name for Anthropic feature opt-ins.
9+
const ANTHROPIC_API_BASE: &str = "https://api.anthropic.com/v1";
10+
/// Pinned `anthropic-version` header value. `2023-06-01` is the
11+
/// version the public Anthropic Messages API is documented against;
12+
/// bumping it changes streaming-event shapes and request fields, so
13+
/// any change here needs a smoke test against both streaming and
14+
/// non-streaming paths.
15+
const ANTHROPIC_API_VERSION: &str = "2023-06-01";
1216
const BETA_HEADER_NAME: &str = "anthropic-beta";
1317

1418
/// Universal beta token: shrinks tool-call envelopes. Supported on
@@ -112,7 +116,10 @@ impl AnthropicClient {
112116
HeaderValue::from_str(&api_key)
113117
.map_err(|e| SofosError::Config(format!("Invalid API key format: {}", e)))?,
114118
);
115-
headers.insert("anthropic-version", HeaderValue::from_static(API_VERSION));
119+
headers.insert(
120+
"anthropic-version",
121+
HeaderValue::from_static(ANTHROPIC_API_VERSION),
122+
);
116123
// `anthropic-beta` is set per-request by `anthropic_beta_for`
117124
// so the compaction beta only ships when the target model
118125
// actually supports it.
@@ -126,7 +133,7 @@ impl AnthropicClient {
126133
pub async fn check_connectivity(&self) -> Result<()> {
127134
utils::check_api_connectivity(
128135
&self.client,
129-
API_BASE,
136+
ANTHROPIC_API_BASE,
130137
"Anthropic",
131138
"https://status.anthropic.com",
132139
)
@@ -157,7 +164,7 @@ impl AnthropicClient {
157164
&self,
158165
request: CreateMessageRequest,
159166
) -> Result<CreateMessageResponse> {
160-
let url = format!("{}/messages", API_BASE);
167+
let url = format!("{}/messages", ANTHROPIC_API_BASE);
161168
let request = Self::prepare_request(request);
162169
let beta = anthropic_beta_for(&request.model);
163170

@@ -189,7 +196,7 @@ impl AnthropicClient {
189196
request.stream = Some(true);
190197
let beta = anthropic_beta_for(&request.model);
191198

192-
let url = format!("{}/messages", API_BASE);
199+
let url = format!("{}/messages", ANTHROPIC_API_BASE);
193200

194201
let response = utils::send_once(
195202
"Anthropic",

src/api/openai.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use serde::Deserialize;
66
use serde_json::json;
77

88
const OPENAI_API_BASE: &str = "https://api.openai.com/v1";
9+
const TOOL_CHOICE_AUTO: &str = "auto";
910

1011
#[derive(Clone)]
1112
pub struct OpenAIClient {
@@ -272,7 +273,7 @@ fn build_responses_body(request: &CreateMessageRequest) -> serde_json::Value {
272273

273274
if !tools.is_empty() {
274275
body["tools"] = json!(tools);
275-
body["tool_choice"] = json!("auto");
276+
body["tool_choice"] = json!(TOOL_CHOICE_AUTO);
276277
}
277278
}
278279

src/clipboard.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,8 @@ pub fn strip_paste_markers(input: &str) -> (String, Vec<usize>) {
4343
(cleaned.trim().to_string(), indices)
4444
}
4545

46-
/// Ceiling on pasted clipboard images. Matches the 20 MB cap Anthropic
47-
/// imposes on base64-encoded image bodies in the Messages API — a
48-
/// larger screenshot would just get rejected at request time with a
49-
/// confusing 400. Checked on both the raw PNG buffer (encoder output)
50-
/// and the encoded base64 so a huge image never makes it into the
51-
/// conversation state.
46+
/// Matches the 20 MB cap Anthropic imposes on base64-encoded image
47+
/// bodies in the Messages API.
5248
const MAX_CLIPBOARD_IMAGE_BYTES: usize = 20 * 1024 * 1024;
5349

5450
pub fn get_clipboard_image() -> Option<PastedImage> {

src/config.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// Central configuration for Sofos. The actual file-size and bash-output
22
/// caps live next to the code that enforces them — `MAX_FILE_SIZE` in
3-
/// `src/tools/filesystem.rs` (50 MB) and `MAX_OUTPUT_SIZE` in
3+
/// `src/tools/filesystem.rs` (50 MB) and `MAX_BASH_OUTPUT_BYTES` in
44
/// `src/tools/bashexec.rs` (10 MB) — not here, so this struct only
55
/// carries config values that the rest of the crate actually reads.
66
///

src/error.rs

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
use thiserror::Error;
22

3+
pub const DEFAULT_PARENT_DIR: &str = ".";
4+
5+
/// Substrings that, when found in a `ToolExecution` error message,
6+
/// classify the error as an *expected* security block rather than an
7+
/// actual failure. Both casings of "blocked" / "Blocked" are listed
8+
/// because tool messages are constructed inline by call sites that
9+
/// don't share a casing convention.
10+
const BLOCKED_KEYWORDS: &[&str] = &[
11+
"blocked",
12+
"Blocked",
13+
"denied",
14+
"not allowed",
15+
"not explicitly allowed",
16+
"outside workspace",
17+
"absolute paths",
18+
"contains '..'",
19+
"tilde paths",
20+
"output redirection",
21+
"here-doc",
22+
];
23+
324
#[derive(Error, Debug)]
425
pub enum SofosError {
526
#[error("API error: {0}")]
@@ -55,19 +76,7 @@ impl SofosError {
5576
pub fn is_blocked(&self) -> bool {
5677
match self {
5778
Self::PathViolation(_) => true,
58-
Self::ToolExecution(msg) => {
59-
msg.contains("blocked")
60-
|| msg.contains("Blocked")
61-
|| msg.contains("denied")
62-
|| msg.contains("not allowed")
63-
|| msg.contains("not explicitly allowed")
64-
|| msg.contains("outside workspace")
65-
|| msg.contains("absolute paths")
66-
|| msg.contains("contains '..'")
67-
|| msg.contains("tilde paths")
68-
|| msg.contains("output redirection")
69-
|| msg.contains("here-doc")
70-
}
79+
Self::ToolExecution(msg) => BLOCKED_KEYWORDS.iter().any(|kw| msg.contains(kw)),
7180
Self::McpError(_) => false,
7281
Self::Join(_) => false,
7382
Self::Context { source, .. } => source.is_blocked(),
@@ -81,7 +90,7 @@ impl SofosError {
8190
let parent = std::path::Path::new(path)
8291
.parent()
8392
.and_then(|p| p.to_str())
84-
.unwrap_or(".");
93+
.unwrap_or(DEFAULT_PARENT_DIR);
8594
Some(format!(
8695
"Use list_directory on '{}' to see available files",
8796
parent

src/repl/conversation.rs

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
use crate::api::{Message, SystemPrompt, utils::truncate_at_char_boundary};
22
use crate::config::SofosConfig;
33

4+
/// Hard floor on the number of messages `trim_if_needed` will keep,
5+
/// even when the per-message budget would normally drop more. Below
6+
/// this, conversations lose enough context that the model starts
7+
/// hallucinating prior tool results.
8+
const TRIM_MIN_MESSAGES: usize = 10;
9+
10+
/// Per-end cap on retained characters when `truncate_tool_results`
11+
/// shortens a long tool output during compaction. The middle is
12+
/// replaced with an elision marker.
13+
const COMPACTION_TOOL_RESULT_KEEP_CHARS: usize = 500;
14+
415
#[derive(Clone)]
516
pub struct ConversationHistory {
617
messages: Vec<Message>,
718
system_prompt: Vec<SystemPrompt>,
819
config: SofosConfig,
9-
/// Set when `trim_if_needed` printed the floor-hit warning; cleared
10-
/// the next time we end a trim under budget. Stops the warning from
11-
/// firing on every message append once we're stuck at the 10-message
12-
/// floor.
20+
/// Latches the floor-hit warning so it fires once per stuck-at-floor
21+
/// episode, not on every append.
1322
warned_at_floor: bool,
1423
/// Index of the message whose last block carries the secondary
1524
/// Anthropic `cache_control` marker (the "anchor"). Stays put across
@@ -300,17 +309,21 @@ Show imperial units only when the user explicitly asks for them."#,
300309
// The warning describes our internal trim heuristic, not the
301310
// model's API context window — those are different numbers.
302311
// The condition below means: we tried to trim down to budget
303-
// but hit the 10-message floor. The model API will still accept
304-
// the request; this just warns the user that auto-trim can't
305-
// help further. Dedup with `warned_at_floor` so a long agent
306-
// loop doesn't print the warning on every tool round-trip.
307-
let at_floor = total_tokens > self.config.max_context_tokens && self.messages.len() <= 10;
312+
// but hit the `TRIM_MIN_MESSAGES` floor. The model API will
313+
// still accept the request; this just warns the user that
314+
// auto-trim can't help further. Dedup with `warned_at_floor`
315+
// so a long agent loop doesn't print the warning on every
316+
// tool round-trip.
317+
let at_floor = total_tokens > self.config.max_context_tokens
318+
&& self.messages.len() <= TRIM_MIN_MESSAGES;
308319
if at_floor {
309320
if !self.warned_at_floor {
310321
eprintln!(
311-
"⚠️ Auto-trim hit the 10-message floor at ~{} tokens (budget {}). \
322+
"⚠️ Auto-trim hit the {floor}-message floor at ~{tokens} tokens (budget {budget}). \
312323
Run /compact or /clear if responses start degrading.",
313-
total_tokens, self.config.max_context_tokens
324+
floor = TRIM_MIN_MESSAGES,
325+
tokens = total_tokens,
326+
budget = self.config.max_context_tokens,
314327
);
315328
self.warned_at_floor = true;
316329
}
@@ -573,7 +586,7 @@ Show imperial units only when the user explicitly asks for them."#,
573586
// stamp a marker on a now-mismatched position.
574587
self.cache_anchor_message_idx = None;
575588
let threshold = self.config.tool_result_truncate_threshold;
576-
let keep_chars = 500;
589+
let keep_chars = COMPACTION_TOOL_RESULT_KEEP_CHARS;
577590

578591
for msg in self.messages[..up_to].iter_mut() {
579592
if let crate::api::MessageContent::Blocks { content } = &mut msg.content {

src/repl/mod.rs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,10 @@ pub struct Repl {
7777
/// iterations so the user can redirect in-flight work without having
7878
/// to interrupt it.
7979
steer_queue: SteerQueue,
80-
/// Text the TUI should print through its captured-stdout pipe right
81-
/// after `OutputCapture` is installed. Collected in `main.rs` (logo,
82-
/// workspace, model, reasoning/thinking, morph availability) so the
83-
/// same lines that used to go to the real tty before the TUI took
84-
/// over now flow through the history pipeline — keeping the viewport
85-
/// from overwriting them on terminals whose cursor-position DSR
86-
/// doesn't answer (e.g. Ghostty), where our fallback was `(0, 0)`.
80+
/// Queued through the TUI's captured-stdout pipe so the banner
81+
/// survives terminals whose cursor-position DSR doesn't answer
82+
/// (e.g. Ghostty), where the fallback origin would let the viewport
83+
/// overwrite the lines.
8784
startup_banner: String,
8885
/// Shared tokio runtime driving every `block_on` in the REPL
8986
/// (initial request, compaction summary, tool-list refresh). Built

src/repl/response_handler.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,6 @@ impl ResponseHandler {
6565
}
6666
}
6767

68-
/// Fold a `Usage` payload into the per-turn running totals carried
69-
/// by `handle_response`. Centralised so the counter increments
70-
/// stay consistent across the three sites that consume responses
71-
/// (auto-continue after reasoning-only blocks, tool-result loop,
72-
/// max-iterations summary).
7368
fn accumulate_usage(
7469
usage: &crate::api::Usage,
7570
total_input: &mut u32,

src/repl/tui/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ use app::{App, Picker};
4040
use event::{Job, UiEvent};
4141
use output::OutputCapture;
4242

43+
/// Background tick cadence — frame rate vs. event-loop responsiveness
44+
/// tradeoff. ~11 Hz is fast enough that streamed output looks fluid
45+
/// without burning CPU on a quiet conversation.
4346
const TICK_INTERVAL: Duration = Duration::from_millis(90);
4447
/// Maximum captured output lines coalesced into a single `insert_before`
4548
/// call. A high value amortises terminal I/O when a tool streams a lot of

0 commit comments

Comments
 (0)