mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-11 03:40:05 +00:00
parent
08a40c3b26
commit
e02a414f69
6 changed files with 842 additions and 10 deletions
260
run.json
260
run.json
File diff suppressed because one or more lines are too long
404
stages/006-simplify_opus@1/diff.patch
Normal file
404
stages/006-simplify_opus@1/diff.patch
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
diff --git a/Cargo.lock b/Cargo.lock
|
||||
index 789a8f4ce..7ec0867dc 100644
|
||||
--- a/Cargo.lock
|
||||
+++ b/Cargo.lock
|
||||
@@ -1629,6 +1629,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"shell-escape",
|
||||
+ "strum",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
diff --git a/lib/crates/fabro-agent/Cargo.toml b/lib/crates/fabro-agent/Cargo.toml
|
||||
index be015c24d..e1fb7d0ab 100644
|
||||
--- a/lib/crates/fabro-agent/Cargo.toml
|
||||
+++ b/lib/crates/fabro-agent/Cargo.toml
|
||||
@@ -38,6 +38,7 @@ fabro-http.workspace = true
|
||||
thiserror.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
+strum.workspace = true
|
||||
tokio.workspace = true
|
||||
uuid.workspace = true
|
||||
futures.workspace = true
|
||||
diff --git a/lib/crates/fabro-agent/src/compaction.rs b/lib/crates/fabro-agent/src/compaction.rs
|
||||
index 7c2434fa7..f610cdd89 100644
|
||||
--- a/lib/crates/fabro-agent/src/compaction.rs
|
||||
+++ b/lib/crates/fabro-agent/src/compaction.rs
|
||||
@@ -13,58 +13,51 @@ use crate::types::{AgentEvent, Message};
|
||||
|
||||
const APPROX_CHARS_PER_TOKEN: usize = 4;
|
||||
|
||||
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
-enum ContextEstimateMethod {
|
||||
+#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::IntoStaticStr)]
|
||||
+#[strum(serialize_all = "snake_case")]
|
||||
+pub(crate) enum ContextEstimateMethod {
|
||||
ApiUsagePlusLocalDelta,
|
||||
LocalEstimate,
|
||||
}
|
||||
|
||||
-impl ContextEstimateMethod {
|
||||
- const fn as_str(self) -> &'static str {
|
||||
- match self {
|
||||
- Self::ApiUsagePlusLocalDelta => "api_usage_plus_local_delta",
|
||||
- Self::LocalEstimate => "local_estimate",
|
||||
- }
|
||||
- }
|
||||
-}
|
||||
-
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
-struct ContextEstimate {
|
||||
- tokens: usize,
|
||||
- method: ContextEstimateMethod,
|
||||
+pub(crate) struct ContextEstimate {
|
||||
+ pub tokens: usize,
|
||||
+ pub method: ContextEstimateMethod,
|
||||
}
|
||||
|
||||
/// Check whether the context window usage exceeds the configured threshold.
|
||||
/// Emits a `Warning` event with kind `"context_window"` when over the
|
||||
-/// threshold. Returns `true` if the threshold is exceeded.
|
||||
-pub fn check_context_usage(
|
||||
+/// threshold. Returns `Some(estimate)` if the threshold is exceeded so the
|
||||
+/// caller can pass it to `compact_context` without recomputing.
|
||||
+pub(crate) fn check_context_usage(
|
||||
system_prompt: &str,
|
||||
history: &History,
|
||||
provider_profile: &dyn AgentProfile,
|
||||
threshold_percent: usize,
|
||||
emitter: &Emitter,
|
||||
session_id: &str,
|
||||
-) -> bool {
|
||||
+) -> Option<ContextEstimate> {
|
||||
let estimate = estimate_active_context_usage(system_prompt, history);
|
||||
- let estimated_tokens = estimate.tokens;
|
||||
let context_window = provider_profile.context_window_size();
|
||||
let threshold = context_window * threshold_percent / 100;
|
||||
|
||||
- if estimated_tokens > threshold {
|
||||
- let usage_percent = estimated_tokens.saturating_mul(100) / context_window;
|
||||
+ if estimate.tokens > threshold {
|
||||
+ let usage_percent = estimate.tokens.saturating_mul(100) / context_window;
|
||||
+ let method: &'static str = estimate.method.into();
|
||||
emitter.emit(session_id.to_owned(), AgentEvent::Warning {
|
||||
kind: "context_window".into(),
|
||||
message: format!("Context window usage: {usage_percent}%"),
|
||||
details: serde_json::json!({
|
||||
- "estimated_tokens": estimated_tokens,
|
||||
+ "estimated_tokens": estimate.tokens,
|
||||
"context_window_size": context_window,
|
||||
"usage_percent": usage_percent,
|
||||
- "estimate_method": estimate.method.as_str(),
|
||||
+ "estimate_method": method,
|
||||
}),
|
||||
});
|
||||
- true
|
||||
+ Some(estimate)
|
||||
} else {
|
||||
- false
|
||||
+ None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,13 +67,13 @@ pub fn check_context_usage(
|
||||
clippy::too_many_arguments,
|
||||
reason = "Context compaction needs explicit history, model, tracking, and emission inputs."
|
||||
)]
|
||||
-pub async fn compact_context(
|
||||
+pub(crate) async fn compact_context(
|
||||
history: &mut History,
|
||||
llm_client: &Client,
|
||||
provider_profile: &dyn AgentProfile,
|
||||
- system_prompt: &str,
|
||||
file_tracker: &FileTracker,
|
||||
preserve_count: usize,
|
||||
+ estimate: ContextEstimate,
|
||||
emitter: &Emitter,
|
||||
session_id: &str,
|
||||
) -> Result<(), Error> {
|
||||
@@ -92,11 +85,9 @@ pub async fn compact_context(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
- let estimate = estimate_active_context_usage(system_prompt, history);
|
||||
- let context_window = provider_profile.context_window_size();
|
||||
emitter.emit(session_id.to_owned(), AgentEvent::CompactionStarted {
|
||||
estimated_tokens: estimate.tokens,
|
||||
- context_window_size: context_window,
|
||||
+ context_window_size: provider_profile.context_window_size(),
|
||||
});
|
||||
|
||||
let turns_to_summarize = &history.turns()[..original_turn_count - preserve_count];
|
||||
@@ -164,7 +155,7 @@ function names, error messages, and exact values. Omit pleasantries and conversa
|
||||
"A different assistant began this task and produced the following summary. \
|
||||
Build on their progress — do not repeat completed steps.\n\n{summary_text}"
|
||||
);
|
||||
- let summary_token_estimate = summary_content.len() / 4;
|
||||
+ let summary_token_estimate = summary_content.len() / APPROX_CHARS_PER_TOKEN;
|
||||
|
||||
history.compact(preserve_count, summary_content);
|
||||
|
||||
@@ -178,16 +169,13 @@ Build on their progress — do not repeat completed steps.\n\n{summary_text}"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
-/// Estimate the total token count of the system prompt and conversation
|
||||
-/// history. Uses a rough heuristic of ~4 characters per token.
|
||||
-pub fn estimate_token_count(system_prompt: &str, history: &History) -> usize {
|
||||
- estimate_local_token_count(system_prompt, history.turns())
|
||||
-}
|
||||
-
|
||||
-fn estimate_active_context_usage(system_prompt: &str, history: &History) -> ContextEstimate {
|
||||
+pub(crate) fn estimate_active_context_usage(
|
||||
+ system_prompt: &str,
|
||||
+ history: &History,
|
||||
+) -> ContextEstimate {
|
||||
let turns = history.turns();
|
||||
if let Some((baseline_index, baseline_tokens)) = latest_assistant_usage_baseline(turns) {
|
||||
- let local_delta = estimate_local_token_count("", &turns[baseline_index + 1..]);
|
||||
+ let local_delta = estimate_turns_local_tokens(&turns[baseline_index + 1..]);
|
||||
return ContextEstimate {
|
||||
tokens: baseline_tokens.saturating_add(local_delta),
|
||||
method: ContextEstimateMethod::ApiUsagePlusLocalDelta,
|
||||
@@ -195,7 +183,8 @@ fn estimate_active_context_usage(system_prompt: &str, history: &History) -> Cont
|
||||
}
|
||||
|
||||
ContextEstimate {
|
||||
- tokens: estimate_local_token_count(system_prompt, turns),
|
||||
+ tokens: estimate_system_prompt_local_tokens(system_prompt)
|
||||
+ + estimate_turns_local_tokens(turns),
|
||||
method: ContextEstimateMethod::LocalEstimate,
|
||||
}
|
||||
}
|
||||
@@ -212,9 +201,12 @@ fn latest_assistant_usage_baseline(turns: &[Message]) -> Option<(usize, usize)>
|
||||
})
|
||||
}
|
||||
|
||||
-fn estimate_local_token_count(system_prompt: &str, turns: &[Message]) -> usize {
|
||||
- let turn_chars: usize = turns.iter().map(estimate_turn_chars).sum();
|
||||
- (system_prompt.len() + turn_chars) / APPROX_CHARS_PER_TOKEN
|
||||
+fn estimate_turns_local_tokens(turns: &[Message]) -> usize {
|
||||
+ turns.iter().map(estimate_turn_chars).sum::<usize>() / APPROX_CHARS_PER_TOKEN
|
||||
+}
|
||||
+
|
||||
+fn estimate_system_prompt_local_tokens(system_prompt: &str) -> usize {
|
||||
+ system_prompt.len() / APPROX_CHARS_PER_TOKEN
|
||||
}
|
||||
|
||||
fn estimate_turn_chars(turn: &Message) -> usize {
|
||||
@@ -364,24 +356,27 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
- fn estimate_token_count_basic() {
|
||||
+ fn estimate_local_token_count_basic() {
|
||||
let mut history = History::default();
|
||||
history.push(Message::User {
|
||||
content: "Hello world".into(), // 11 chars
|
||||
timestamp: SystemTime::now(),
|
||||
});
|
||||
- // system_prompt = "test" (4 chars) + 11 chars = 15 chars / 4 = 3 tokens
|
||||
- assert_eq!(estimate_token_count("test", &history), 3);
|
||||
+ // system_prompt = "test" (4/4 = 1 token) + 11 chars / 4 = 2 tokens = 3 tokens
|
||||
+ let estimate = estimate_active_context_usage("test", &history);
|
||||
+ assert_eq!(estimate.tokens, 3);
|
||||
+ assert_eq!(estimate.method, ContextEstimateMethod::LocalEstimate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
- fn active_context_estimate_without_assistant_usage_matches_local_history_estimate() {
|
||||
+ fn active_context_estimate_without_assistant_usage_uses_local_estimate() {
|
||||
let mut history = History::default();
|
||||
history.push(Message::User {
|
||||
- content: "Hello world".into(),
|
||||
+ content: "Hello world".into(), // 11 chars => 2 tokens
|
||||
timestamp: SystemTime::now(),
|
||||
});
|
||||
history.push(Message::Assistant {
|
||||
+ // 18 chars content + tool call name (9) + args (16) = 43 chars => 10 tokens
|
||||
content: "No usage available".into(),
|
||||
tool_calls: vec![ToolCall::new(
|
||||
"call_1",
|
||||
@@ -394,14 +389,16 @@ mod tests {
|
||||
timestamp: SystemTime::now(),
|
||||
});
|
||||
history.push(Message::ToolResults {
|
||||
+ // 4 chars => 1 token
|
||||
results: vec![ToolResult::success("call_1", serde_json::json!(1234))],
|
||||
timestamp: SystemTime::now(),
|
||||
});
|
||||
|
||||
let estimate = estimate_active_context_usage("test", &history);
|
||||
|
||||
- assert_eq!(estimate.tokens, estimate_token_count("test", &history));
|
||||
assert_eq!(estimate.method, ContextEstimateMethod::LocalEstimate);
|
||||
+ // sysprompt 4/4 = 1, turns sum = (11 + 18 + 9 + 16 + 4) / 4 = 58/4 = 14
|
||||
+ assert_eq!(estimate.tokens, 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -523,8 +520,8 @@ mod tests {
|
||||
let emitter = Emitter::new();
|
||||
let profile = TestProfile::new();
|
||||
// Empty history, huge context window => well below threshold
|
||||
- let over = check_context_usage("short", &history, &profile, 80, &emitter, "sess");
|
||||
- assert!(!over);
|
||||
+ let result = check_context_usage("short", &history, &profile, 80, &emitter, "sess");
|
||||
+ assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -539,8 +536,8 @@ mod tests {
|
||||
let mut rx = emitter.subscribe();
|
||||
// TestProfile has context_window=200_000 by default; use a small one
|
||||
let profile = TestProfile::with_context_window(ToolRegistry::new(), 100);
|
||||
- let over = check_context_usage("prompt", &history, &profile, 80, &emitter, "sess");
|
||||
- assert!(over);
|
||||
+ let result = check_context_usage("prompt", &history, &profile, 80, &emitter, "sess");
|
||||
+ assert!(result.is_some());
|
||||
|
||||
// Should have emitted a Warning
|
||||
let event = rx.try_recv().unwrap();
|
||||
diff --git a/lib/crates/fabro-agent/src/history.rs b/lib/crates/fabro-agent/src/history.rs
|
||||
index 33182ae73..e9dd10476 100644
|
||||
--- a/lib/crates/fabro-agent/src/history.rs
|
||||
+++ b/lib/crates/fabro-agent/src/history.rs
|
||||
@@ -32,12 +32,17 @@ impl History {
|
||||
self.turns.iter().map(Message::to_session_message).collect()
|
||||
}
|
||||
|
||||
+ /// Compact the history by replacing all but the trailing `preserve_count`
|
||||
+ /// turns with a summary `System` message. Preserved assistant turns have
|
||||
+ /// their `usage` reset to default so a later context-window estimate does
|
||||
+ /// not treat pre-compaction provider-reported usage as the new baseline;
|
||||
+ /// authoritative billing is recorded via emitted run events.
|
||||
pub fn compact(&mut self, preserve_count: usize, summary: String) {
|
||||
if self.turns.len() <= preserve_count {
|
||||
return;
|
||||
}
|
||||
let mut preserved = self.turns.split_off(self.turns.len() - preserve_count);
|
||||
- invalidate_assistant_usage(&mut preserved);
|
||||
+ Self::invalidate_preserved_usage(&mut preserved);
|
||||
let discarded = std::mem::take(&mut self.turns);
|
||||
let extracted_user_messages =
|
||||
extract_recent_user_messages(discarded, COMPACTION_USER_MESSAGE_TOKEN_BUDGET);
|
||||
@@ -50,6 +55,14 @@ impl History {
|
||||
self.strip_opaque_provider_items();
|
||||
}
|
||||
|
||||
+ fn invalidate_preserved_usage(preserved: &mut [Message]) {
|
||||
+ for turn in preserved {
|
||||
+ if let Message::Assistant { usage, .. } = turn {
|
||||
+ **usage = TokenCounts::default();
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
/// Remove provider-specific opaque items that are no longer valid after
|
||||
/// compaction. OpenAI reasoning and message items are opaque round-trip
|
||||
/// data tied to specific API responses; after compaction replaces their
|
||||
@@ -120,14 +133,6 @@ impl History {
|
||||
}
|
||||
}
|
||||
|
||||
-fn invalidate_assistant_usage(turns: &mut [Message]) {
|
||||
- for turn in turns {
|
||||
- if let Message::Assistant { usage, .. } = turn {
|
||||
- **usage = TokenCounts::default();
|
||||
- }
|
||||
- }
|
||||
-}
|
||||
-
|
||||
/// Maximum token budget for user messages extracted from discarded turns during
|
||||
/// compaction.
|
||||
const COMPACTION_USER_MESSAGE_TOKEN_BUDGET: usize = 20_000;
|
||||
diff --git a/lib/crates/fabro-agent/src/session.rs b/lib/crates/fabro-agent/src/session.rs
|
||||
index de9c58093..d999666e4 100644
|
||||
--- a/lib/crates/fabro-agent/src/session.rs
|
||||
+++ b/lib/crates/fabro-agent/src/session.rs
|
||||
@@ -1670,31 +1670,34 @@ impl Session {
|
||||
}
|
||||
|
||||
async fn compact_if_needed(&mut self) {
|
||||
- let over_threshold = check_context_usage(
|
||||
+ let Some(estimate) = check_context_usage(
|
||||
&self.system_prompt,
|
||||
&self.history,
|
||||
self.provider_profile.as_ref(),
|
||||
self.config.compaction_threshold_percent,
|
||||
&self.event_emitter,
|
||||
&self.id,
|
||||
- );
|
||||
- if over_threshold && self.config.enable_context_compaction {
|
||||
- if let Err(e) = compact_context(
|
||||
- &mut self.history,
|
||||
- &self.llm_client,
|
||||
- self.provider_profile.as_ref(),
|
||||
- &self.system_prompt,
|
||||
- &self.file_tracker,
|
||||
- self.config.compaction_preserve_turns,
|
||||
- &self.event_emitter,
|
||||
- &self.id,
|
||||
- )
|
||||
- .await
|
||||
- {
|
||||
- self.event_emitter.emit(self.id.clone(), AgentEvent::Error {
|
||||
- error: Error::InvalidState(format!("Context compaction failed: {e}")),
|
||||
- });
|
||||
- }
|
||||
+ ) else {
|
||||
+ return;
|
||||
+ };
|
||||
+ if !self.config.enable_context_compaction {
|
||||
+ return;
|
||||
+ }
|
||||
+ if let Err(e) = compact_context(
|
||||
+ &mut self.history,
|
||||
+ &self.llm_client,
|
||||
+ self.provider_profile.as_ref(),
|
||||
+ &self.file_tracker,
|
||||
+ self.config.compaction_preserve_turns,
|
||||
+ estimate,
|
||||
+ &self.event_emitter,
|
||||
+ &self.id,
|
||||
+ )
|
||||
+ .await
|
||||
+ {
|
||||
+ self.event_emitter.emit(self.id.clone(), AgentEvent::Error {
|
||||
+ error: Error::InvalidState(format!("Context compaction failed: {e}")),
|
||||
+ });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3658,9 +3661,9 @@ mod tests {
|
||||
response
|
||||
}
|
||||
|
||||
- fn response_with_total_usage(response: Response, total_tokens: i64) -> Response {
|
||||
+ fn response_with_input_tokens(response: Response, input_tokens: i64) -> Response {
|
||||
response_with_usage(response, TokenCounts {
|
||||
- input_tokens: total_tokens,
|
||||
+ input_tokens,
|
||||
..TokenCounts::default()
|
||||
})
|
||||
}
|
||||
@@ -3719,7 +3722,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn compaction_uses_assistant_usage_baseline_for_short_response() {
|
||||
let responses = vec![
|
||||
- response_with_total_usage(text_response("OK"), 90),
|
||||
+ response_with_input_tokens(text_response("OK"), 90),
|
||||
text_response("Here is the summary of the conversation so far."),
|
||||
text_response("fallback"),
|
||||
];
|
||||
@@ -3833,7 +3836,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn compaction_disabled_blocks_api_usage_baseline_compaction() {
|
||||
- let responses = vec![response_with_total_usage(text_response("OK"), 90)];
|
||||
+ let responses = vec![response_with_input_tokens(text_response("OK"), 90)];
|
||||
|
||||
let provider = Arc::new(MockLlmProvider::new(responses));
|
||||
let client = make_client(provider).await;
|
||||
6
stages/006-simplify_opus@1/status.json
Normal file
6
stages/006-simplify_opus@1/status.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"outcome": "succeeded",
|
||||
"notes": "Stage completed: simplify_opus",
|
||||
"failure_reason": null,
|
||||
"timestamp": "2026-05-23T15:43:53.788041Z"
|
||||
}
|
||||
159
stages/007-simplify_gpt@1/prompt.md
Normal file
159
stages/007-simplify_gpt@1/prompt.md
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
Goal: # Agent Compaction API Usage Baseline Plan
|
||||
|
||||
Date: 2026-05-23
|
||||
|
||||
## Summary
|
||||
|
||||
Change Fabro's agent compaction trigger from a whole-history `chars / 4`
|
||||
estimate to a Claude Code-style hot-path estimate: use the latest real
|
||||
assistant response's stored `usage.total_tokens()` as the baseline, then add
|
||||
local estimates for turns appended after that response. This avoids token-count
|
||||
provider API calls while making compaction sensitive to actual
|
||||
provider-reported context usage, including cache and reasoning tokens.
|
||||
|
||||
No provider token-count API calls should be added in this change.
|
||||
|
||||
## Key Changes
|
||||
|
||||
- Replace the current compaction estimate in `fabro-agent` with a new
|
||||
active-context estimator.
|
||||
- Find the newest assistant turn whose `usage.total_tokens() > 0`.
|
||||
- Use that `usage.total_tokens()` as the baseline.
|
||||
- Add local estimates only for turns after that assistant turn.
|
||||
- If no usable assistant usage exists, fall back to the existing local
|
||||
whole-history estimate.
|
||||
- Reuse a shared per-turn local estimate helper so fallback and post-baseline
|
||||
delta counting stay consistent.
|
||||
- Keep the estimator local and in-process. Do not call
|
||||
`llm_client.count_input_tokens()` from `compact_if_needed()`.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
- Update `lib/crates/fabro-agent/src/compaction.rs`:
|
||||
- Add an estimator that returns both token count and method, for example
|
||||
`ApiUsagePlusLocalDelta` or `LocalEstimate`.
|
||||
- Make `check_context_usage()` use the new estimator and include the method
|
||||
in warning `details`.
|
||||
- Make `compact_context()` report the same improved estimate in
|
||||
`CompactionStarted`.
|
||||
- Move `CompactionStarted` emission after the
|
||||
`original_turn_count <= preserve_count` no-op check, so a no-op compact
|
||||
cannot emit started without completed.
|
||||
- Update `lib/crates/fabro-agent/src/history.rs`:
|
||||
- In `History::compact()`, invalidate preserved assistant usage by replacing
|
||||
preserved assistant `usage` with `TokenCounts::default()`.
|
||||
- Keep provider parts, response IDs, text, and tool calls unchanged.
|
||||
- Rationale: preserved assistant usage reflects the pre-compaction context and
|
||||
must not become the next baseline. Billing remains available from emitted
|
||||
run events, so mutable runtime history should prefer compaction correctness.
|
||||
- Leave public run event names and schemas unchanged:
|
||||
- `agent.compaction.started`
|
||||
- `agent.compaction.completed`
|
||||
- Existing warning event remains a warning with richer `details`.
|
||||
|
||||
## Test Plan
|
||||
|
||||
- Add unit coverage in `lib/crates/fabro-agent/src/compaction.rs`:
|
||||
- No assistant usage: estimator matches current local whole-history behavior.
|
||||
- Latest assistant usage present: estimator uses `usage.total_tokens()` plus
|
||||
only later tool/user/steering turns.
|
||||
- Usage fields include cache and reasoning through `TokenCounts::total_tokens()`.
|
||||
- Earlier assistant usage is ignored when a later assistant usage exists.
|
||||
- Add unit coverage in `lib/crates/fabro-agent/src/history.rs`:
|
||||
- `History::compact()` preserves assistant content, tool calls, and provider
|
||||
parts, but resets preserved assistant usage to default.
|
||||
- Existing OpenAI opaque stripping and Anthropic thinking preservation tests
|
||||
still pass.
|
||||
- Add session coverage in `lib/crates/fabro-agent/src/session.rs`:
|
||||
- A short assistant response with high `usage.total_tokens()` triggers
|
||||
compaction even when text length is small.
|
||||
- A compact no-op due to `turns.len() <= preserve_count` does not emit
|
||||
`CompactionStarted`.
|
||||
- Compaction disabled still prevents compaction even if the API usage
|
||||
baseline exceeds threshold.
|
||||
- Run targeted verification:
|
||||
- `cargo nextest run -p fabro-agent compaction`
|
||||
- `cargo nextest run -p fabro-agent history`
|
||||
- If those pass, run `cargo nextest run -p fabro-agent`.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- Runtime/session `Message::Assistant.usage` is safe to invalidate after
|
||||
compaction because authoritative billing comes from emitted workflow/run
|
||||
events, not preserved mutable agent history.
|
||||
- A zero-token `TokenCounts::default()` should be treated as no usable API
|
||||
baseline.
|
||||
- Provider token-count APIs remain available for future near-threshold
|
||||
confirmation, but are intentionally out of scope for this change.
|
||||
|
||||
|
||||
## Completed stages
|
||||
- **toolchain**: succeeded
|
||||
- Script: `command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1`
|
||||
- Output:
|
||||
```
|
||||
cargo 1.95.0 (f2d3ce0bd 2026-03-21)
|
||||
```
|
||||
- **preflight_compile**: succeeded
|
||||
- Script: `cargo check -q --workspace 2>&1`
|
||||
- Output: (empty)
|
||||
- **preflight_lint**: succeeded
|
||||
- Script: `cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1`
|
||||
- Output: (empty)
|
||||
- **implement**: succeeded
|
||||
- Model: gpt-5.5, 168.6k tokens in / 21.5k out
|
||||
- **simplify_opus**: succeeded
|
||||
- Model: claude-opus-4-7, 66.7k tokens in / 21.3k out
|
||||
- Files: /home/daytona/workspace/fabro/lib/crates/fabro-agent/Cargo.toml, /home/daytona/workspace/fabro/lib/crates/fabro-agent/src/compaction.rs, /home/daytona/workspace/fabro/lib/crates/fabro-agent/src/history.rs, /home/daytona/workspace/fabro/lib/crates/fabro-agent/src/session.rs
|
||||
|
||||
|
||||
# Simplify: Code Review and Cleanup
|
||||
|
||||
Review changes vs. origin for reuse, quality, and efficiency. Fix any issues found.
|
||||
|
||||
## Phase 1: Identify Changes
|
||||
|
||||
Run git diff (or git diff HEAD if there are staged changes) to see what changed. If there are no git changes, review the most recently modified files that the user mentioned or that you edited earlier in this conversation.
|
||||
|
||||
## Phase 2: Launch Three Review Agents in Parallel
|
||||
|
||||
Use the Agent tool to launch all three agents concurrently in a single message. Pass each agent the full diff so it has the complete context.
|
||||
|
||||
### Agent 1: Code Reuse Review
|
||||
|
||||
For each change:
|
||||
|
||||
1. Search for existing utilities and helpers that could replace newly written code. Use Grep to find similar patterns elsewhere in the codebase — common locations are utility directories, shared modules, and files adjacent to the changed ones.
|
||||
2. Flag any new function that duplicates existing functionality. Suggest the existing function to use instead.
|
||||
3. Flag any inline logic that could use an existing utility — hand-rolled string manipulation, manual path handling, custom environment checks, ad-hoc type guards, and similar patterns are common candidates.
|
||||
|
||||
Note: This is a greenfield app, so focus on maximizing simplicity and don't worry about changing things to achieve it.
|
||||
|
||||
### Agent 2: Code Quality Review
|
||||
|
||||
Review the same changes for hacky patterns:
|
||||
|
||||
1. Redundant state: state that duplicates existing state, cached values that could be derived, observers/effects that could be direct calls
|
||||
2. Parameter sprawl: adding new parameters to a function instead of generalizing or restructuring existing ones
|
||||
3. Copy-paste with slight variation: near-duplicate code blocks that should be unified with a shared abstraction
|
||||
4. Leaky abstractions: exposing internal details that should be encapsulated, or breaking existing abstraction boundaries
|
||||
5. Stringly-typed code: using raw strings where constants, enums (string unions), or branded types already exist in the codebase
|
||||
|
||||
Note: This is a greenfield app, so be aggressive in optimizing quality.
|
||||
|
||||
### Agent 3: Efficiency Review
|
||||
|
||||
Review the same changes for efficiency:
|
||||
|
||||
1. Unnecessary work: redundant computations, repeated file reads, duplicate network/API calls, N+1 patterns
|
||||
2. Missed concurrency: independent operations run sequentially when they could run in parallel
|
||||
3. Hot-path bloat: new blocking work added to startup or per-request/per-render hot paths
|
||||
4. Unnecessary existence checks: pre-checking file/resource existence before operating (TOCTOU anti-pattern) — operate directly and handle the error
|
||||
5. Memory: unbounded data structures, missing cleanup, event listener leaks
|
||||
6. Overly broad operations: reading entire files when only a portion is needed, loading all items when filtering for one
|
||||
|
||||
## Phase 3: Fix Issues
|
||||
|
||||
Wait for all three agents to complete. Aggregate their findings and fix each issue directly. If a finding is a false positive or not worth addressing, note it and move on — do not argue with the finding, just skip it.
|
||||
|
||||
When done, briefly summarize what was fixed (or confirm the code was already clean).
|
||||
5
stages/007-simplify_gpt@1/provider_used.json
Normal file
5
stages/007-simplify_gpt@1/provider_used.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"mode": "agent",
|
||||
"provider": "openai",
|
||||
"model": "gpt-5.5"
|
||||
}
|
||||
18
stages/007-simplify_gpt@1/response.md
Normal file
18
stages/007-simplify_gpt@1/response.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
Reviewed the compaction changes vs `origin/main` and launched the three requested parallel review agents.
|
||||
|
||||
Fixed one correctness/quality issue found during review:
|
||||
|
||||
- `estimate_active_context_usage()` local fallback was dividing the system prompt and turns separately, which could differ from the previous whole-history `chars / 4` behavior due to integer rounding.
|
||||
- Updated it to sum system prompt chars + turn chars first, then divide once.
|
||||
- Reused the same local char/token helper for fallback, post-baseline delta, and summary token estimate.
|
||||
- Added a regression test proving whole-history rounding is preserved.
|
||||
|
||||
Verification run:
|
||||
|
||||
- `cargo nextest run -p fabro-agent compaction` ✅
|
||||
- `cargo nextest run -p fabro-agent history` ✅
|
||||
- `cargo +nightly-2026-04-14 fmt --check --all` ✅
|
||||
- `cargo nextest run -p fabro-agent` ✅ 448 passed
|
||||
- `cargo +nightly-2026-04-14 clippy -q --package fabro-agent --tests --no-deps -- -D warnings` ✅
|
||||
|
||||
Note: a broader `cargo +nightly-2026-04-14 clippy -q -p fabro-agent --all-targets -- -D warnings` hit an unrelated dependency lint in `fabro-sandbox::SandboxSpec` (`large_enum_variant`), so I reran clippy scoped to `fabro-agent` without dependencies, which passed.
|
||||
Loading…
Add table
Reference in a new issue