Skip to content

Commit 9bea098

Browse files
feat: add core/git/ai/plugin/telemetry crates, dynamic version from git tag in CI
1 parent 525fada commit 9bea098

40 files changed

Lines changed: 3969 additions & 1 deletion

.github/workflows/release.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,20 @@ jobs:
5555
libsoup-3.0-dev \
5656
libjavascriptcoregtk-4.1-dev
5757
58+
- name: Set version from tag
59+
if: startsWith(github.ref, 'refs/tags/v')
60+
shell: bash
61+
run: |
62+
VERSION="${GITHUB_REF#refs/tags/v}"
63+
echo "Setting version to $VERSION"
64+
# Update workspace Cargo.toml
65+
sed -i.bak "s/^version = \".*\"/version = \"$VERSION\"/" Cargo.toml
66+
# Update each crate's Cargo.toml
67+
for f in crates/*/Cargo.toml; do
68+
sed -i.bak "s/^version = \".*\"/version = \"$VERSION\"/" "$f" 2>/dev/null || true
69+
done
70+
rm -f Cargo.toml.bak crates/*/Cargo.toml.bak
71+
5872
- name: Install frontend dependencies
5973
working-directory: crates/tauri-app/frontend
6074
run: npm ci --ignore-scripts || npm install

Cargo.lock

Lines changed: 61 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
[workspace]
22
resolver = "2"
3-
members = ["crates/tauri-app", "crates/session", "crates/persistence"]
3+
members = [
4+
"crates/tauri-app",
5+
"crates/session",
6+
"crates/persistence",
7+
"crates/core",
8+
"crates/git",
9+
"crates/ai",
10+
"crates/plugin",
11+
"crates/telemetry",
12+
]
13+
# Only build app crates by default — library crates are standalone
14+
default-members = [
15+
"crates/tauri-app",
16+
"crates/session",
17+
"crates/persistence",
18+
]
419

520
[workspace.package]
621
version = "0.1.0"

crates/ai/Cargo.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "codeforge-ai"
3+
version.workspace = true
4+
edition.workspace = true
5+
license.workspace = true
6+
description = "AI provider abstraction and message types for CodeForge"
7+
8+
[dependencies]
9+
serde = { workspace = true }
10+
serde_json = { workspace = true }
11+
uuid = { workspace = true }
12+
chrono = { workspace = true }
13+
anyhow = { workspace = true }
14+
thiserror = { workspace = true }
15+
tokio = { workspace = true }

crates/ai/src/context.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
//! Context window tracking and compaction strategies.
2+
3+
use serde::{Deserialize, Serialize};
4+
5+
/// Tracks token usage within the AI context window.
6+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
7+
pub struct ContextWindow {
8+
/// Maximum tokens the model supports.
9+
pub capacity: u64,
10+
/// Tokens used by the system prompt.
11+
pub system_tokens: u64,
12+
/// Tokens used by conversation messages.
13+
pub message_tokens: u64,
14+
/// Tokens used by tool definitions.
15+
pub tool_tokens: u64,
16+
/// Tokens reserved for the model's response.
17+
pub response_reserve: u64,
18+
}
19+
20+
impl ContextWindow {
21+
/// Create a new context window with the given capacity.
22+
pub fn new(capacity: u64) -> Self {
23+
Self {
24+
capacity,
25+
..Default::default()
26+
}
27+
}
28+
29+
/// Returns the total tokens currently used.
30+
pub fn used(&self) -> u64 {
31+
self.system_tokens + self.message_tokens + self.tool_tokens
32+
}
33+
34+
/// Returns the number of tokens available for new messages.
35+
pub fn available(&self) -> u64 {
36+
self.capacity
37+
.saturating_sub(self.used())
38+
.saturating_sub(self.response_reserve)
39+
}
40+
41+
/// Returns the usage as a fraction (0.0 to 1.0).
42+
pub fn usage_fraction(&self) -> f64 {
43+
if self.capacity == 0 {
44+
return 0.0;
45+
}
46+
self.used() as f64 / self.capacity as f64
47+
}
48+
49+
/// Returns `true` if context compaction should be triggered.
50+
///
51+
/// Defaults to triggering at 90% usage.
52+
pub fn needs_compaction(&self) -> bool {
53+
self.usage_fraction() > 0.9
54+
}
55+
56+
/// Returns `true` if the context window is effectively full
57+
/// (less than 5% remaining after response reserve).
58+
pub fn is_full(&self) -> bool {
59+
self.available() < (self.capacity / 20)
60+
}
61+
}
62+
63+
impl std::fmt::Display for ContextWindow {
64+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65+
let pct = (self.usage_fraction() * 100.0) as u32;
66+
write!(
67+
f,
68+
"{}/{} tokens ({}% used, {} available)",
69+
self.used(),
70+
self.capacity,
71+
pct,
72+
self.available()
73+
)
74+
}
75+
}
76+
77+
/// Trait for implementing context compaction strategies.
78+
pub trait ContextManager {
79+
/// The error type for compaction operations.
80+
type Error: std::error::Error;
81+
82+
/// Compact the context by summarizing or removing older messages.
83+
///
84+
/// Returns the number of tokens freed.
85+
fn compact(&self, window: &ContextWindow) -> Result<CompactionResult, Self::Error>;
86+
87+
/// Estimate the token count for a given text.
88+
fn estimate_tokens(&self, text: &str) -> u64;
89+
}
90+
91+
/// The result of a context compaction operation.
92+
#[derive(Debug, Clone, Serialize, Deserialize)]
93+
pub struct CompactionResult {
94+
/// Number of tokens freed by compaction.
95+
pub tokens_freed: u64,
96+
/// Number of messages removed or summarized.
97+
pub messages_affected: usize,
98+
/// The compaction strategy that was used.
99+
pub strategy: CompactionStrategy,
100+
}
101+
102+
/// Available strategies for compacting the context window.
103+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
104+
#[serde(rename_all = "snake_case")]
105+
pub enum CompactionStrategy {
106+
/// Remove the oldest messages.
107+
TruncateOldest,
108+
/// Summarize older messages into a condensed form.
109+
Summarize,
110+
/// Remove tool result content but keep tool use records.
111+
StripToolResults,
112+
/// Remove thinking blocks from assistant messages.
113+
StripThinking,
114+
}

crates/ai/src/cost.rs

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
//! Token usage tracking and cost calculation.
2+
3+
use chrono::{DateTime, Utc};
4+
use serde::{Deserialize, Serialize};
5+
use std::fmt;
6+
7+
/// A report of token usage for a single API call.
8+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
9+
pub struct UsageReport {
10+
/// Number of input tokens consumed.
11+
pub input_tokens: u64,
12+
/// Number of output tokens generated.
13+
pub output_tokens: u64,
14+
/// Number of tokens used for caching (cache reads).
15+
pub cache_read_tokens: u64,
16+
/// Number of tokens written to cache.
17+
pub cache_write_tokens: u64,
18+
/// The model that was used.
19+
pub model: Option<String>,
20+
/// Timestamp of this usage event.
21+
pub timestamp: Option<DateTime<Utc>>,
22+
}
23+
24+
impl UsageReport {
25+
/// Returns the total token count (input + output).
26+
pub fn total_tokens(&self) -> u64 {
27+
self.input_tokens + self.output_tokens
28+
}
29+
}
30+
31+
impl fmt::Display for UsageReport {
32+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33+
write!(
34+
f,
35+
"in: {} | out: {} | total: {}",
36+
self.input_tokens,
37+
self.output_tokens,
38+
self.total_tokens()
39+
)?;
40+
if self.cache_read_tokens > 0 {
41+
write!(f, " | cache read: {}", self.cache_read_tokens)?;
42+
}
43+
Ok(())
44+
}
45+
}
46+
47+
/// Accumulates usage across multiple API calls within a session.
48+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
49+
pub struct CostTracker {
50+
/// All usage reports accumulated so far.
51+
reports: Vec<UsageReport>,
52+
/// Running total of input tokens.
53+
total_input: u64,
54+
/// Running total of output tokens.
55+
total_output: u64,
56+
/// Running total of cache read tokens.
57+
total_cache_read: u64,
58+
/// Running total of cache write tokens.
59+
total_cache_write: u64,
60+
/// Number of API calls made.
61+
api_calls: u64,
62+
}
63+
64+
impl CostTracker {
65+
/// Create a new empty cost tracker.
66+
pub fn new() -> Self {
67+
Self::default()
68+
}
69+
70+
/// Record a new usage report.
71+
pub fn record(&mut self, report: UsageReport) {
72+
self.total_input += report.input_tokens;
73+
self.total_output += report.output_tokens;
74+
self.total_cache_read += report.cache_read_tokens;
75+
self.total_cache_write += report.cache_write_tokens;
76+
self.api_calls += 1;
77+
self.reports.push(report);
78+
}
79+
80+
/// Returns the total input tokens across all API calls.
81+
pub fn total_input_tokens(&self) -> u64 {
82+
self.total_input
83+
}
84+
85+
/// Returns the total output tokens across all API calls.
86+
pub fn total_output_tokens(&self) -> u64 {
87+
self.total_output
88+
}
89+
90+
/// Returns the grand total of all tokens.
91+
pub fn total_tokens(&self) -> u64 {
92+
self.total_input + self.total_output
93+
}
94+
95+
/// Returns the number of API calls tracked.
96+
pub fn api_call_count(&self) -> u64 {
97+
self.api_calls
98+
}
99+
100+
/// Returns the average tokens per API call.
101+
pub fn avg_tokens_per_call(&self) -> f64 {
102+
if self.api_calls == 0 {
103+
return 0.0;
104+
}
105+
self.total_tokens() as f64 / self.api_calls as f64
106+
}
107+
108+
/// Estimate the cost in USD based on model pricing.
109+
///
110+
/// Uses approximate pricing: input $3/MTok, output $15/MTok for Sonnet-class models.
111+
pub fn estimated_cost_usd(&self) -> f64 {
112+
let input_cost = self.total_input as f64 * 3.0 / 1_000_000.0;
113+
let output_cost = self.total_output as f64 * 15.0 / 1_000_000.0;
114+
let cache_read_cost = self.total_cache_read as f64 * 0.30 / 1_000_000.0;
115+
let cache_write_cost = self.total_cache_write as f64 * 3.75 / 1_000_000.0;
116+
input_cost + output_cost + cache_read_cost + cache_write_cost
117+
}
118+
119+
/// Returns a reference to all recorded usage reports.
120+
pub fn reports(&self) -> &[UsageReport] {
121+
&self.reports
122+
}
123+
124+
/// Reset the tracker, clearing all accumulated data.
125+
pub fn reset(&mut self) {
126+
*self = Self::default();
127+
}
128+
}
129+
130+
impl fmt::Display for CostTracker {
131+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132+
write!(
133+
f,
134+
"{} calls | {} total tokens | ~${:.4}",
135+
self.api_calls,
136+
self.total_tokens(),
137+
self.estimated_cost_usd()
138+
)
139+
}
140+
}

0 commit comments

Comments
 (0)