refactor(rust): rename crate from 'contexa' to 'cortexa'

Swadhin Biswas committed 7 months ago
ff86ca4
1 parent af28f1c
commit ff86ca4
7 files changed
+255 -53
▼RUST/Cargo.lock
+40-7
66
66
]
67
67
68
68
[[package]]
69
-name = "contexa"
69
+name = "core-foundation-sys"
70
+version = "0.8.7"
71
+source = "registry+https://github.com/rust-lang/crates.io-index"
72
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
73
+
74
+[[package]]
75
+name = "cortexa"
70
76
version = "0.1.1"
71
77
dependencies = [
72
78
"chrono",
79
+ "fs2",
73
80
"serde",
74
81
"serde_yaml",
75
82
"tempfile",
77
84
"uuid",
78
85
]
79
86
80
-[[package]]
81
-name = "core-foundation-sys"
82
-version = "0.8.7"
83
-source = "registry+https://github.com/rust-lang/crates.io-index"
84
-checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
85
-
86
87
[[package]]
87
88
name = "equivalent"
88
89
version = "1.0.2"
117
118
source = "registry+https://github.com/rust-lang/crates.io-index"
118
119
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
119
120
121
+[[package]]
122
+name = "fs2"
123
+version = "0.4.3"
124
+source = "registry+https://github.com/rust-lang/crates.io-index"
125
+checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
126
+dependencies = [
127
+ "libc",
128
+ "winapi",
129
+]
130
+
120
131
[[package]]
121
132
name = "getrandom"
122
133
version = "0.4.2"
551
562
"semver",
552
563
]
553
564
565
+[[package]]
566
+name = "winapi"
567
+version = "0.3.9"
568
+source = "registry+https://github.com/rust-lang/crates.io-index"
569
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
570
+dependencies = [
571
+ "winapi-i686-pc-windows-gnu",
572
+ "winapi-x86_64-pc-windows-gnu",
573
+]
574
+
575
+[[package]]
576
+name = "winapi-i686-pc-windows-gnu"
577
+version = "0.4.0"
578
+source = "registry+https://github.com/rust-lang/crates.io-index"
579
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
580
+
581
+[[package]]
582
+name = "winapi-x86_64-pc-windows-gnu"
583
+version = "0.4.0"
584
+source = "registry+https://github.com/rust-lang/crates.io-index"
585
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
586
+
554
587
[[package]]
555
588
name = "windows-core"
556
589
version = "0.62.2"
557
590
▼RUST/Cargo.toml
+4-3
1
1
[package]
2
-name = "contexa"
2
+name = "cortexa"
3
3
version = "0.1.1"
4
4
edition = "2021"
5
5
description = "Git-inspired context management for LLM agents -- COMMIT, BRANCH, MERGE, and CONTEXT over versioned memory (arXiv:2508.00031)"
8
8
keywords = ["llm", "agent", "context-management", "git", "memory"]
9
9
categories = ["data-structures", "filesystem"]
10
10
readme = "README.md"
11
-repository = "https://github.com/swadhinbiswas/contexa"
12
-homepage = "https://github.com/swadhinbiswas/contexa"
11
+repository = "https://github.com/swadhinbiswas/Cortexa"
12
+homepage = "https://github.com/swadhinbiswas/Cortexa"
13
13
14
14
[dependencies]
15
15
serde = { version = "1", features = ["derive"] }
17
17
chrono = { version = "0.4", features = ["serde"] }
18
18
uuid = { version = "1", features = ["v4"] }
19
19
thiserror = "1"
20
+fs2 = "0.4"
20
21
21
22
[dev-dependencies]
22
23
tempfile = "3"
23
24
▼RUST/README.md
+13-13
1
-# contexa
1
+# cortexa
2
2
3
-[![Crates.io](https://img.shields.io/crates/v/contexa.svg)](https://crates.io/crates/contexa)
4
-[![docs.rs](https://docs.rs/contexa/badge.svg)](https://docs.rs/contexa)
3
+[![Crates.io](https://img.shields.io/crates/v/cortexa.svg)](https://crates.io/crates/cortexa)
4
+[![docs.rs](https://docs.rs/cortexa/badge.svg)](https://docs.rs/cortexa)
5
5
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
6
-[![GitHub](https://img.shields.io/badge/GitHub-swadhinbiswas%2Fcontexa-black.svg?logo=github)](https://github.com/swadhinbiswas/contexa)
6
+[![GitHub](https://img.shields.io/badge/GitHub-swadhinbiswas%2FCortexa-black.svg?logo=github)](https://github.com/swadhinbiswas/Cortexa)
7
7
8
8
**Git-inspired context management for LLM agents.** COMMIT, BRANCH, MERGE, and CONTEXT operations over a persistent versioned memory workspace.
9
9
10
-Rust implementation of the **contexa** framework.
10
+Rust implementation of the **cortexa** framework.
11
11
12
12
Based on: [arXiv:2508.00031](https://arxiv.org/abs/2508.00031) -- *"Git Context Controller: Manage the Context of LLM-based Agents like Git"* (Junde Wu et al., 2025)
13
13
17
17
18
18
```toml
19
19
[dependencies]
20
-contexa = "0.1"
20
+cortexa = "0.1"
21
21
```
22
22
23
23
---
25
25
## Quick Start
26
26
27
27
```rust
28
-use contexa::GCCWorkspace;
28
+use cortexa::GCCWorkspace;
29
29
30
-fn main() -> contexa::Result<()> {
30
+fn main() -> cortexa::Result<()> {
31
31
let mut ws = GCCWorkspace::new("/path/to/project");
32
32
ws.init("Build a REST API service with user auth")?;
33
33
172
172
## Running Tests
173
173
174
174
```bash
175
-git clone https://github.com/swadhinbiswas/contexa.git
176
-cd contexa/RUST
175
+git clone https://github.com/swadhinbiswas/Cortexa.git
176
+cd cortexa/RUST
177
177
cargo test
178
178
```
179
179
211
211
212
212
## Links
213
213
214
-- [GitHub Repository](https://github.com/swadhinbiswas/contexa)
215
-- [crates.io](https://crates.io/crates/contexa)
216
-- [docs.rs](https://docs.rs/contexa)
214
+- [GitHub Repository](https://github.com/swadhinbiswas/Cortexa)
215
+- [crates.io](https://crates.io/crates/cortexa)
216
+- [docs.rs](https://docs.rs/cortexa)
217
217
- [Original Paper](https://arxiv.org/abs/2508.00031)
218
218
- [Author: Swadhin Biswas](https://github.com/swadhinbiswas)
219
219
▼RUST/src/error.rs
+6-0
14
14
#[error("Branch '{name}' not found")]
15
15
BranchNotFound { name: String },
16
16
17
+ #[error("Failed to acquire lock: {0}")]
18
+ Lock(String),
19
+
20
+ #[error("Validation error: {0}")]
21
+ Validation(String),
22
+
17
23
#[error("IO error: {0}")]
18
24
Io(#[from] std::io::Error),
19
25
20
26
▼RUST/src/lib.rs
+2-2
1
-//! # contexa
1
+//! # cortexa
2
2
//!
3
3
//! Git-inspired context management for LLM agents. COMMIT, BRANCH, MERGE, and
4
4
//! CONTEXT operations over a persistent versioned memory workspace.
21
21
//!
22
22
//! ## Example
23
23
//! ```rust,no_run
24
-//! use contexa::GCCWorkspace;
24
+//! use cortexa::GCCWorkspace;
25
25
//!
26
26
//! let mut ws = GCCWorkspace::new("/path/to/project");
27
27
//! ws.init("Build a production REST API").unwrap();
28
28
▼RUST/src/models.rs
+40-4
2
2
3
3
use serde::{Deserialize, Serialize};
4
4
5
+/// Escape the `---` block separator so user content cannot break parsers.
6
+pub fn sanitize(text: &str) -> String {
7
+ text.replace("\n---\n", "\n\\---\n")
8
+}
9
+
10
+/// Reverse the escaping applied by [`sanitize`].
11
+pub fn desanitize(text: &str) -> String {
12
+ text.replace("\n\\---\n", "\n---\n")
13
+}
14
+
15
+/// Split markdown text on `---\n` separators while respecting escaped
16
+/// separators (`\---\n` produced by [`sanitize`]). After splitting,
17
+/// the escaped backslash is left in place; callers should apply
18
+/// [`desanitize`] on individual field values.
19
+pub fn split_blocks(text: &str) -> Vec<String> {
20
+ let raw: Vec<&str> = text.split("---\n").collect();
21
+ let mut blocks: Vec<String> = Vec::new();
22
+ let mut i = 0;
23
+ while i < raw.len() {
24
+ let mut block = raw[i].to_string();
25
+ // If block ends with '\', the `---\n` was an escaped separator — rejoin.
26
+ while block.ends_with('\\') && i + 1 < raw.len() {
27
+ i += 1;
28
+ block.push_str("---\n");
29
+ block.push_str(raw[i]);
30
+ }
31
+ blocks.push(block);
32
+ i += 1;
33
+ }
34
+ blocks
35
+}
36
+
5
37
/// A single Observation–Thought–Action step logged to `log.md`.
6
38
/// The paper continuously logs OTA cycles as the agent executes.
7
39
#[derive(Debug, Clone, Serialize, Deserialize)]
17
49
pub fn to_markdown(&self) -> String {
18
50
format!(
19
51
"### Step {} — {}\n**Observation:** {}\n\n**Thought:** {}\n\n**Action:** {}\n\n---\n",
20
- self.step, self.timestamp, self.observation, self.thought, self.action
52
+ self.step,
53
+ self.timestamp,
54
+ sanitize(&self.observation),
55
+ sanitize(&self.thought),
56
+ sanitize(&self.action),
21
57
)
22
58
}
23
59
}
42
78
**This Commit's Contribution:** {}\n\n---\n",
43
79
self.commit_id,
44
80
self.timestamp,
45
- self.branch_purpose,
46
- self.previous_progress_summary,
47
- self.this_commit_contribution,
81
+ sanitize(&self.branch_purpose),
82
+ sanitize(&self.previous_progress_summary),
83
+ sanitize(&self.this_commit_contribution),
48
84
)
49
85
}
50
86
}
51
87
▼RUST/src/workspace.rs
+150-24
4
4
use std::path::{Path, PathBuf};
5
5
6
6
use chrono::Utc;
7
+use fs2::FileExt;
7
8
use uuid::Uuid;
8
9
9
10
use crate::error::{GCCError, Result};
10
-use crate::models::{BranchMetadata, CommitRecord, ContextResult, OTARecord};
11
+use crate::models::{
12
+ desanitize, split_blocks, BranchMetadata, CommitRecord, ContextResult, OTARecord,
13
+};
11
14
12
15
const MAIN_BRANCH: &str = "main";
13
16
const GCC_DIR: &str = ".GCC";
20
23
Uuid::new_v4().to_string()[..8].to_string()
21
24
}
22
25
26
+/// Return `Err(Validation)` if `value` is empty or whitespace-only.
27
+fn validate_not_empty(value: &str, field: &str) -> Result<()> {
28
+ if value.trim().is_empty() {
29
+ return Err(GCCError::Validation(format!("{field} must not be empty")));
30
+ }
31
+ Ok(())
32
+}
33
+
34
+/// Return `Err(Validation)` if `name` is not a valid branch identifier.
35
+fn validate_branch_name(name: &str) -> Result<()> {
36
+ validate_not_empty(name, "Branch name")?;
37
+ if name.contains('/') || name.contains('\\') {
38
+ return Err(GCCError::Validation(format!(
39
+ "Branch name must not contain path separators: {name:?}"
40
+ )));
41
+ }
42
+ if name == "." || name == ".." {
43
+ return Err(GCCError::Validation(format!(
44
+ "Branch name must not be '.' or '..': {name:?}"
45
+ )));
46
+ }
47
+ Ok(())
48
+}
49
+
23
50
/// Manages the `.GCC/` directory structure for one agent project.
24
51
///
25
52
/// Implements the four GCC commands from arXiv:2508.00031v2:
28
55
/// - MERGE (§3.4): synthesise divergent paths
29
56
/// - CONTEXT (§3.5): hierarchical memory retrieval
30
57
pub struct GCCWorkspace {
31
- root: PathBuf,
32
58
gcc_dir: PathBuf,
33
59
current_branch: String,
34
60
}
35
61
62
+/// RAII file lock guard. Acquires exclusive lock on creation, releases on drop.
63
+struct FileLock {
64
+ _file: fs::File,
65
+}
66
+
67
+impl FileLock {
68
+ fn acquire(gcc_dir: &Path) -> Result<Self> {
69
+ let lock_path = gcc_dir.join(".lock");
70
+ if let Some(parent) = lock_path.parent() {
71
+ fs::create_dir_all(parent)?;
72
+ }
73
+ let file = fs::OpenOptions::new()
74
+ .create(true)
75
+ .read(true)
76
+ .write(true)
77
+ .open(&lock_path)?;
78
+ file.lock_exclusive()
79
+ .map_err(|e| GCCError::Lock(e.to_string()))?;
80
+ Ok(Self { _file: file })
81
+ }
82
+}
83
+
84
+impl Drop for FileLock {
85
+ fn drop(&mut self) {
86
+ let _ = self._file.unlock();
87
+ }
88
+}
89
+
36
90
impl GCCWorkspace {
37
91
pub fn new(project_root: impl AsRef<Path>) -> Self {
38
- let root = project_root.as_ref().to_path_buf();
39
- let gcc_dir = root.join(GCC_DIR);
92
+ let gcc_dir = project_root.as_ref().join(GCC_DIR);
40
93
Self {
41
- root,
42
94
gcc_dir,
43
95
current_branch: MAIN_BRANCH.to_string(),
44
96
}
97
149
fn read_commits(&self, branch: &str) -> Vec<CommitRecord> {
98
150
let text = self.read(&self.commit_path(branch));
99
151
let mut records = Vec::new();
100
- for block in text.split("---\n") {
152
+ for block in split_blocks(&text) {
101
153
let block = block.trim();
102
154
if block.is_empty() {
103
155
continue;
104
156
}
105
157
let mut commit_id = String::new();
106
- let mut branch_purpose = String::new();
107
- let mut prev_summary = String::new();
108
- let mut contribution = String::new();
109
158
let mut ts = String::new();
159
+ let mut current_field: Option<&str> = None;
160
+ let mut fields: std::collections::HashMap<&str, String> =
161
+ std::collections::HashMap::new();
162
+
110
163
for line in block.lines() {
111
164
if line.starts_with("## Commit `") {
112
165
commit_id = line
113
166
.trim_start_matches("## Commit `")
114
167
.trim_end_matches('`')
115
168
.to_string();
169
+ current_field = None;
116
170
} else if line.starts_with("**Timestamp:**") {
117
171
ts = line.replace("**Timestamp:**", "").trim().to_string();
172
+ current_field = None;
118
173
} else if line.starts_with("**Branch Purpose:**") {
119
- branch_purpose = line.replace("**Branch Purpose:**", "").trim().to_string();
174
+ let val = line.replace("**Branch Purpose:**", "").trim().to_string();
175
+ fields.insert("branch_purpose", val);
176
+ current_field = Some("branch_purpose");
120
177
} else if line.starts_with("**Previous Progress Summary:**") {
121
- prev_summary = line
178
+ let val = line
122
179
.replace("**Previous Progress Summary:**", "")
123
180
.trim()
124
181
.to_string();
182
+ fields.insert("prev_summary", val);
183
+ current_field = Some("prev_summary");
125
184
} else if line.starts_with("**This Commit's Contribution:**") {
126
- contribution = line
185
+ let val = line
127
186
.replace("**This Commit's Contribution:**", "")
128
187
.trim()
129
188
.to_string();
189
+ fields.insert("contribution", val);
190
+ current_field = Some("contribution");
191
+ } else if let Some(field) = current_field {
192
+ let trimmed = line.trim();
193
+ if !trimmed.is_empty() {
194
+ if let Some(existing) = fields.get_mut(field) {
195
+ existing.push('\n');
196
+ existing.push_str(trimmed);
197
+ }
198
+ }
130
199
}
131
200
}
132
201
if !commit_id.is_empty() {
202
+ let get = |key: &str| -> String {
203
+ desanitize(fields.get(key).map(|s| s.trim()).unwrap_or(""))
204
+ };
133
205
records.push(CommitRecord {
134
206
commit_id,
135
207
branch_name: branch.to_string(),
136
- branch_purpose,
137
- previous_progress_summary: prev_summary,
138
- this_commit_contribution: contribution,
208
+ branch_purpose: get("branch_purpose"),
209
+ previous_progress_summary: get("prev_summary"),
210
+ this_commit_contribution: get("contribution"),
139
211
timestamp: ts,
140
212
});
141
213
}
146
218
fn read_ota(&self, branch: &str) -> Vec<OTARecord> {
147
219
let text = self.read(&self.log_path(branch));
148
220
let mut records = Vec::new();
149
- for block in text.split("---\n") {
221
+ for block in split_blocks(&text) {
150
222
let block = block.trim();
151
223
if block.is_empty() {
152
224
continue;
153
225
}
154
226
let mut step = 0usize;
155
227
let mut ts = String::new();
156
- let mut obs = String::new();
157
- let mut thought = String::new();
158
- let mut action = String::new();
228
+ let mut current_field: Option<&str> = None;
229
+ let mut fields: std::collections::HashMap<&str, String> =
230
+ std::collections::HashMap::new();
231
+
159
232
for line in block.lines() {
160
233
if line.starts_with("### Step ") {
161
234
let parts: Vec<&str> = line.splitn(2, '—').collect();
165
238
.parse()
166
239
.unwrap_or(0);
167
240
ts = parts.get(1).unwrap_or(&"").trim().to_string();
241
+ current_field = None;
168
242
} else if line.starts_with("**Observation:**") {
169
- obs = line.replace("**Observation:**", "").trim().to_string();
243
+ let val = line.replace("**Observation:**", "").trim().to_string();
244
+ fields.insert("obs", val);
245
+ current_field = Some("obs");
170
246
} else if line.starts_with("**Thought:**") {
171
- thought = line.replace("**Thought:**", "").trim().to_string();
247
+ let val = line.replace("**Thought:**", "").trim().to_string();
248
+ fields.insert("thought", val);
249
+ current_field = Some("thought");
172
250
} else if line.starts_with("**Action:**") {
173
- action = line.replace("**Action:**", "").trim().to_string();
251
+ let val = line.replace("**Action:**", "").trim().to_string();
252
+ fields.insert("action", val);
253
+ current_field = Some("action");
254
+ } else if let Some(field) = current_field {
255
+ let trimmed = line.trim();
256
+ if !trimmed.is_empty() {
257
+ if let Some(existing) = fields.get_mut(field) {
258
+ existing.push('\n');
259
+ existing.push_str(trimmed);
260
+ }
261
+ }
174
262
}
175
263
}
264
+ let get = |key: &str| -> String {
265
+ desanitize(fields.get(key).map(|s| s.trim()).unwrap_or(""))
266
+ };
267
+ let obs = get("obs");
268
+ let thought = get("thought");
176
269
if !obs.is_empty() || !thought.is_empty() {
177
270
records.push(OTARecord {
178
271
step,
179
272
timestamp: ts,
180
273
observation: obs,
181
274
thought,
182
- action,
275
+ action: get("action"),
183
276
});
184
277
}
185
278
}
209
302
let main_dir = self.branch_dir(MAIN_BRANCH);
210
303
fs::create_dir_all(&main_dir)?;
211
304
305
+ let _lock = FileLock::acquire(&self.gcc_dir)?;
306
+
212
307
// main.md — global roadmap
213
308
let roadmap = format!(
214
309
"# Project Roadmap\n\n**Initialized:** {}\n\n{}\n",
261
356
/// Append an OTA step to the current branch's `log.md`.
262
357
/// The paper logs continuous Observation–Thought–Action cycles.
263
358
pub fn log_ota(&self, observation: &str, thought: &str, action: &str) -> Result<OTARecord> {
359
+ if observation.trim().is_empty() && thought.trim().is_empty() && action.trim().is_empty() {
360
+ return Err(GCCError::Validation(
361
+ "At least one of observation, thought, or action must be non-empty".to_string(),
362
+ ));
363
+ }
364
+ let _lock = FileLock::acquire(&self.gcc_dir)?;
264
365
let existing = self.read_ota(&self.current_branch);
265
366
let step = existing.len() + 1;
266
367
let record = OTARecord {
283
384
contribution: &str,
284
385
previous_summary: Option<&str>,
285
386
update_roadmap: Option<&str>,
387
+ ) -> Result<CommitRecord> {
388
+ validate_not_empty(contribution, "Contribution")?;
389
+ let _lock = FileLock::acquire(&self.gcc_dir)?;
390
+ self.commit_inner(contribution, previous_summary, update_roadmap)
391
+ }
392
+
393
+ /// Internal commit logic, called with lock already held.
394
+ fn commit_inner(
395
+ &self,
396
+ contribution: &str,
397
+ previous_summary: Option<&str>,
398
+ update_roadmap: Option<&str>,
286
399
) -> Result<CommitRecord> {
287
400
let meta = self.read_meta(&self.current_branch);
288
401
let branch_purpose = meta.as_ref().map(|m| m.purpose.clone()).unwrap_or_default();
325
438
/// Creates isolated workspace: B_t^(name) = BRANCH(M_{t-1}).
326
439
/// Initialises empty OTA trace and commit.md; metadata records intent.
327
440
pub fn branch(&mut self, name: &str, purpose: &str) -> Result<()> {
441
+ validate_branch_name(name)?;
442
+ validate_not_empty(purpose, "Branch purpose")?;
328
443
let branch_dir = self.branch_dir(name);
329
444
if branch_dir.exists() {
330
445
return Err(GCCError::BranchExists {
333
448
}
334
449
335
450
fs::create_dir_all(&branch_dir)?;
451
+
452
+ let _lock = FileLock::acquire(&self.gcc_dir)?;
336
453
self.write(
337
454
&self.log_path(name),
338
455
&format!("# OTA Log — branch `{name}`\n\n"),
367
484
summary: Option<&str>,
368
485
target: &str,
369
486
) -> Result<CommitRecord> {
487
+ validate_branch_name(branch_name)?;
488
+ validate_branch_name(target)?;
370
489
if !self.branch_dir(branch_name).exists() {
371
490
return Err(GCCError::BranchNotFound {
372
491
name: branch_name.to_string(),
373
492
});
374
493
}
375
494
495
+ let _lock = FileLock::acquire(&self.gcc_dir)?;
496
+
376
497
let branch_commits = self.read_commits(branch_name);
377
498
let branch_ota = self.read_ota(branch_name);
378
499
let meta = self.read_meta(branch_name);
409
530
branch_name,
410
531
meta.as_ref().map(|m| m.purpose.as_str()).unwrap_or("")
411
532
);
412
- let merge_commit = self.commit(&merge_summary, Some(&prev), Some(&merge_summary))?;
533
+ let merge_commit = self.commit_inner(&merge_summary, Some(&prev), Some(&merge_summary))?;
413
534
414
535
// Mark branch as merged
415
536
if let Some(mut m) = meta {
427
548
/// Retrieves historical context at K-commit resolution.
428
549
/// Paper experiments use K=1 (only most recent commit revealed).
429
550
pub fn context(&self, branch: Option<&str>, k: usize) -> Result<ContextResult> {
551
+ if k < 1 {
552
+ return Err(GCCError::Validation(format!("k must be >= 1, got {k}")));
553
+ }
554
+ let _lock = FileLock::acquire(&self.gcc_dir)?;
430
555
let target = branch.unwrap_or(&self.current_branch);
431
556
if !self.branch_dir(target).exists() {
432
557
return Err(GCCError::BranchNotFound {
464
589
}
465
590
466
591
pub fn switch_branch(&mut self, name: &str) -> Result<()> {
592
+ validate_branch_name(name)?;
467
593
if !self.branch_dir(name).exists() {
468
594
return Err(GCCError::BranchNotFound {
469
595
name: name.to_string(),
470
596