cobolx 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Cargo.lock CHANGED
@@ -1098,7 +1098,7 @@ dependencies = [
1098
1098
 
1099
1099
  [[package]]
1100
1100
  name = "rdo"
1101
- version = "1.0.2"
1101
+ version = "1.0.4"
1102
1102
  dependencies = [
1103
1103
  "chrono",
1104
1104
  "clap",
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rdo"
3
- version = "1.0.3"
3
+ version = "1.0.4"
4
4
  edition = "2024"
5
5
 
6
6
  [dependencies]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cobolx",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "CobolX CLI",
5
5
  "main": "bin/cobolx.js",
6
6
  "bin": {
@@ -3,9 +3,12 @@ use super::clients::{DeepSeekClient, GlmClient};
3
3
  // Re-exports — tui.rs and other in-crate code uses `use crate::agent::client::{...}`
4
4
  pub use super::types::{ChatMessage, Route, Usage};
5
5
  use crate::config::ConfigManager;
6
+ use crate::memory::{CodexMemories, MemoryStore};
6
7
  use crate::ui::tui::{Message, Sender};
7
8
  use std::path::Path;
8
9
 
10
+ const SUMMARY_LOG_MAX_BYTES: usize = 12_000;
11
+
9
12
  pub struct AgentRouter {
10
13
  pub(crate) deepseek: Option<DeepSeekClient>,
11
14
  pub(crate) glm: Option<GlmClient>,
@@ -91,14 +94,44 @@ impl AgentRouter {
91
94
  }
92
95
  }
93
96
 
94
- fn build_messages(history: &[Message]) -> Vec<ChatMessage> {
97
+ fn load_prompt_memory(sandbox_path: Option<&Path>) -> (Option<String>, Option<String>) {
98
+ match sandbox_path {
99
+ None => (None, None),
100
+ Some(p) => MemoryStore::open_or_create(p)
101
+ .ok()
102
+ .map(|store| {
103
+ let mem = store.codex_memories();
104
+ let agents = mem.read_agents_instructions();
105
+ let summary = mem.read_memory_summary_for_injection().ok();
106
+ (agents, summary)
107
+ })
108
+ .unwrap_or((None, None)),
109
+ }
110
+ }
111
+
112
+ fn build_messages(
113
+ history: &[Message],
114
+ agents_instructions: Option<&str>,
115
+ memory_summary: Option<&str>,
116
+ ) -> Vec<ChatMessage> {
117
+ let mut system_text = String::from(
118
+ "You are COBOLX, a helpful assistant. COBOLX is a migration agent for legacy \
119
+ COBOL systems based on DeepSeek.",
120
+ );
121
+ if let Some(agents) = agents_instructions {
122
+ system_text.push_str("\n\n## Project instructions (AGENTS.md)\n\n");
123
+ system_text.push_str(agents);
124
+ }
125
+ if let Some(summary) = memory_summary {
126
+ system_text.push_str(
127
+ "\n\n## Persisted memory summary (memory_summary.md)\n\
128
+ Codex-style navigational memory for continuity:\n\n",
129
+ );
130
+ system_text.push_str(summary);
131
+ }
95
132
  let mut messages = vec![ChatMessage {
96
133
  role: "system".to_string(),
97
- content: Some(
98
- "You are COBOLX, a helpful assistant. COBOLX is a migration agent for legacy \
99
- COBOL systems based on DeepSeek."
100
- .to_string(),
101
- ),
134
+ content: Some(system_text),
102
135
  tool_call_id: None,
103
136
  tool_calls: None,
104
137
  }];
@@ -130,6 +163,59 @@ impl AgentRouter {
130
163
  messages
131
164
  }
132
165
 
166
+ /// Codex Phase 2: consolidate rollout log into `memory_summary.md` + `MEMORY.md`.
167
+ pub async fn consolidate_codex_memories(
168
+ &self,
169
+ memory_summary: &str,
170
+ memory_handbook: &str,
171
+ rollout_log: &str,
172
+ tokens_summarized: u32,
173
+ ) -> Result<(String, String), String> {
174
+ let truncated_log = truncate_utf8_tail(rollout_log, SUMMARY_LOG_MAX_BYTES);
175
+ let system_msg = ChatMessage {
176
+ role: "system".to_string(),
177
+ content: Some(
178
+ "You are the COBOLX memory consolidation agent (Codex Phase 2). \
179
+ Merge existing memory with a new rollout log. Output ONLY two markdown \
180
+ documents separated by exact markers:\n\
181
+ ---COBOLX_MEMORY_SUMMARY---\n\
182
+ (short navigational summary, like Codex memory_summary.md)\n\
183
+ ---COBOLX_MEMORY_HANDBOOK---\n\
184
+ (long-form handbook, like Codex MEMORY.md)\n\
185
+ Include last_updated (ISO8601 UTC) and tokens_summarized (cumulative) in the summary. \
186
+ Be concise. Preserve useful manual edits. Focus on COBOL programs, user goals, \
187
+ sandbox facts, and decisions."
188
+ .to_string(),
189
+ ),
190
+ tool_call_id: None,
191
+ tool_calls: None,
192
+ };
193
+ let user_msg = ChatMessage {
194
+ role: "user".to_string(),
195
+ content: Some(format!(
196
+ "tokens_to_add: {}\n\n\
197
+ Existing memory_summary.md:\n\n{}\n\n---\n\n\
198
+ Existing MEMORY.md:\n\n{}\n\n---\n\n\
199
+ New rollout log:\n\n{}",
200
+ tokens_summarized, memory_summary, memory_handbook, truncated_log
201
+ )),
202
+ tool_call_id: None,
203
+ tool_calls: None,
204
+ };
205
+ let messages = vec![system_msg, user_msg];
206
+
207
+ let output = if let Some(ref ds) = self.deepseek {
208
+ ds.call_api(&messages, Some(0.2)).await?
209
+ } else if let Some(ref g) = self.glm {
210
+ g.call_api(&messages, Some(0.2)).await?
211
+ } else {
212
+ return Err("No API client available for memory consolidation.".to_string());
213
+ };
214
+
215
+ CodexMemories::parse_consolidation_output(&output)
216
+ .ok_or_else(|| "Consolidation output missing required markers.".to_string())
217
+ }
218
+
133
219
  #[allow(dead_code)]
134
220
  pub async fn execute_chat(
135
221
  &self,
@@ -137,7 +223,8 @@ impl AgentRouter {
137
223
  route: Route,
138
224
  _sandbox_path: Option<&Path>,
139
225
  ) -> Result<(String, &'static str), String> {
140
- let messages = Self::build_messages(history);
226
+ let (agents, memory_summary) = Self::load_prompt_memory(_sandbox_path);
227
+ let messages = Self::build_messages(history, agents.as_deref(), memory_summary.as_deref());
141
228
  match route {
142
229
  Route::Light => {
143
230
  if let Some(ref ds) = self.deepseek {
@@ -174,7 +261,8 @@ impl AgentRouter {
174
261
  sandbox_path: Option<&Path>,
175
262
  tx: tokio::sync::mpsc::UnboundedSender<String>,
176
263
  ) -> Result<(Option<Usage>, &'static str), String> {
177
- let messages = Self::build_messages(history);
264
+ let (agents, memory_summary) = Self::load_prompt_memory(sandbox_path);
265
+ let messages = Self::build_messages(history, agents.as_deref(), memory_summary.as_deref());
178
266
  match route {
179
267
  Route::Light => {
180
268
  if let Some(ref ds) = self.deepseek {
@@ -248,6 +336,17 @@ impl AgentRouter {
248
336
  }
249
337
  }
250
338
 
339
+ fn truncate_utf8_tail(content: &str, max_bytes: usize) -> &str {
340
+ if content.len() <= max_bytes {
341
+ return content;
342
+ }
343
+ let mut start = content.len() - max_bytes;
344
+ while start < content.len() && !content.is_char_boundary(start) {
345
+ start += 1;
346
+ }
347
+ &content[start..]
348
+ }
349
+
251
350
  #[cfg(test)]
252
351
  mod tests {
253
352
  use super::*;
@@ -1,4 +1,5 @@
1
1
  use super::AgentRouter;
2
+ use super::skills::{AgentKind, append_agent_skills};
2
3
  use super::types::merge_tool_call_deltas;
3
4
  use super::types::{
4
5
  ChatMessage, ChatRequest, FunctionDefinition, StreamOptions, Tool, ToolCall, Usage,
@@ -6,6 +7,27 @@ use super::types::{
6
7
  use crate::memory::MemoryStore;
7
8
  use std::path::Path;
8
9
 
10
+ fn build_database_query_tool() -> Tool {
11
+ Tool {
12
+ r#type: "function".to_string(),
13
+ function: FunctionDefinition {
14
+ name: "query_sqlite".to_string(),
15
+ description: "Run one read-only SELECT query against the indexed project SQLite database. Use this for project facts from files, programs, data_items, call_edges, copybook_uses, program_features, code_blocks, external_ops, identifiers, literals, copybook_features, and other indexed COBOL metadata. Do not use it for writes, DDL, or guesses."
16
+ .to_string(),
17
+ parameters: serde_json::json!({
18
+ "type": "object",
19
+ "properties": {
20
+ "sql": {
21
+ "type": "string",
22
+ "description": "A single SQLite SELECT statement that reads indexed project data."
23
+ }
24
+ },
25
+ "required": ["sql"]
26
+ }),
27
+ },
28
+ }
29
+ }
30
+
9
31
  impl AgentRouter {
10
32
  pub(crate) async fn run_database_agent_stream(
11
33
  &self,
@@ -34,7 +56,7 @@ impl AgentRouter {
34
56
 
35
57
  if let Some(first_msg) = messages.get_mut(0) {
36
58
  if first_msg.role == "system" {
37
- first_msg.content = Some(
59
+ let mut system_prompt =
38
60
  "You are the COBOLX Database Sub-Agent. Your task is to help the user analyze \
39
61
  their COBOL codebase by querying the local SQLite database. You have access to \
40
62
  the `query_sqlite` tool to execute read-only SELECT queries.\n\
@@ -47,33 +69,35 @@ impl AgentRouter {
47
69
  kind 'static'|'dynamic', using_count)\n\
48
70
  5. `data_items` (id, program_id, source_file_id, name, level, parent_name, \
49
71
  pic, usage_clause, occurs, redefines, section, byte_offset, byte_size, \
50
- storage_kind, layout_status, start_offset, byte_len)\n\n\
72
+ storage_kind, layout_status, start_offset, byte_len)\n\
73
+ 6. `program_features` (program_id, source_file_id, incoming_call_count, \
74
+ outgoing_call_count, static_call_count, dynamic_call_count, \
75
+ copybook_use_count, distinct_copybook_count, referenced_by_file_count, \
76
+ is_entrypoint, has_heavy_copy_usage, data_item_count, paragraph_count, \
77
+ external_op_count, identifier_count, literal_count)\n\
78
+ 7. `code_blocks` (id, program_id, source_file_id, name, kind \
79
+ 'section'|'paragraph', parent_section, sequence_no, statement_count, \
80
+ start_offset, byte_len)\n\
81
+ 8. `external_ops` (id, program_id, source_file_id, kind, verb, target, \
82
+ start_offset, byte_len)\n\
83
+ 9. `identifiers` (id, program_id, source_file_id, kind, value, occurrences, \
84
+ first_offset)\n\
85
+ 10. `literals` (id, program_id, source_file_id, kind, value, occurrences, \
86
+ first_offset)\n\
87
+ 11. `copybook_features` (copybook_file_id, copybook_name, \
88
+ used_by_program_count, used_by_file_count, replacing_use_count, \
89
+ data_item_count, contains_header_fields, contains_error_fields)\n\n\
51
90
  GUIDELINES:\n\
52
91
  - Write standard SELECT queries only (read-only).\n\
53
92
  - If unsure about columns, query the schema first.\n\
54
93
  - Explain answers clearly; if no data matches, say so."
55
- .to_string(),
56
- );
94
+ .to_string();
95
+ append_agent_skills(&mut system_prompt, sandbox_path, AgentKind::Database)?;
96
+ first_msg.content = Some(system_prompt);
57
97
  }
58
98
  }
59
99
 
60
- let query_sqlite_tool = Tool {
61
- r#type: "function".to_string(),
62
- function: FunctionDefinition {
63
- name: "query_sqlite".to_string(),
64
- description: "Run a read-only SELECT query against the local SQLite database \
65
- indexing the COBOL project structure."
66
- .to_string(),
67
- parameters: serde_json::json!({
68
- "type": "object",
69
- "properties": {
70
- "sql": { "type": "string", "description": "The SQLite SELECT statement." }
71
- },
72
- "required": ["sql"]
73
- }),
74
- },
75
- };
76
- let tools = vec![query_sqlite_tool];
100
+ let tools = vec![build_database_query_tool()];
77
101
  let mut final_usage = Usage::default();
78
102
 
79
103
  for _turn in 0..10 {
@@ -185,8 +209,15 @@ impl AgentRouter {
185
209
  .get("sql")
186
210
  .and_then(|v| v.as_str())
187
211
  .unwrap_or("");
188
- let db_result = match store.query_readonly(sql) {
189
- Ok(json_val) => json_val.to_string(),
212
+ let db_result = match store.project_index_is_empty() {
213
+ Ok(true) => serde_json::json!({
214
+ "error": "Project index is empty. Run /init before asking database questions."
215
+ })
216
+ .to_string(),
217
+ Ok(false) => match store.query_readonly(sql) {
218
+ Ok(json_val) => json_val.to_string(),
219
+ Err(err) => serde_json::json!({ "error": err.to_string() }).to_string(),
220
+ },
190
221
  Err(err) => serde_json::json!({ "error": err.to_string() }).to_string(),
191
222
  };
192
223
  messages.push(ChatMessage {
@@ -204,3 +235,20 @@ impl AgentRouter {
204
235
  Ok(Some(final_usage))
205
236
  }
206
237
  }
238
+
239
+ #[cfg(test)]
240
+ mod tests {
241
+ use super::*;
242
+
243
+ #[test]
244
+ fn database_query_tool_description_spells_out_readonly_scope() {
245
+ let tool = build_database_query_tool();
246
+ assert!(tool.function.description.contains("SELECT"));
247
+ assert!(tool.function.description.contains("read-only"));
248
+ assert!(
249
+ tool.function.description.contains(
250
+ "files, programs, data_items, call_edges, copybook_uses, program_features"
251
+ )
252
+ );
253
+ }
254
+ }
@@ -1,10 +1,12 @@
1
1
  use super::AgentRouter;
2
+ use super::skills::{AgentKind, append_agent_skills};
2
3
  use super::types::merge_tool_call_deltas;
3
4
  use super::types::{
4
- ChatMessage, ChatRequest, ChatResponse, FunctionDefinition, StreamOptions, Tool, ToolCall,
5
- Usage,
5
+ ChatMessage, ChatRequest, ChatResponse, FunctionDefinition, SharedWriteBuffer, StreamOptions,
6
+ Tool, ToolCall, Usage, WriteBuffer,
6
7
  };
7
8
  use std::path::Path;
9
+ use std::sync::Arc;
8
10
 
9
11
  impl AgentRouter {
10
12
  /// Verify agent: reviews a draft answer and returns (passed, feedback).
@@ -13,6 +15,7 @@ impl AgentRouter {
13
15
  user_question: &str,
14
16
  gathered_data: &str,
15
17
  draft_content: &str,
18
+ sandbox_path: &Path,
16
19
  ) -> Result<(bool, String), String> {
17
20
  let (api_key, api_url, model_name) = if let Some(ref g) = self.glm {
18
21
  (
@@ -30,12 +33,15 @@ impl AgentRouter {
30
33
  return Err("No API client for Verify Agent.".to_string());
31
34
  };
32
35
 
33
- let system_prompt = "You are the COBOLX Verify Agent. Review the draft answer.\n\
34
- Look for: incomplete paragraphs, missing values, TODOs, incorrect analysis, \
35
- grammar/logic issues, missing files/variables the user asked about.\n\
36
+ let mut system_prompt = "You are the COBOLX Verify Agent. Review the draft answer.\n\
37
+ Look for:\n\
38
+ 1. Incomplete paragraphs, missing values, TODOs, incorrect analysis, grammar/logic issues.\n\
39
+ 2. Missing files/variables the user asked about.\n\
40
+ 3. File naming preservation: ensure any documentation written via write_file uses the original base name of the COBOL source file (e.g. docs/XYZ.md for source XYZ.cbl) instead of generic names like entity.md or model.md.\n\
36
41
  Return a JSON object ONLY (no markdown wrapping):\n\
37
42
  { \"passed\": bool, \"feedback\": \"...\" }"
38
43
  .to_string();
44
+ append_agent_skills(&mut system_prompt, sandbox_path, AgentKind::Verify)?;
39
45
 
40
46
  let user_prompt = format!(
41
47
  "User Question:\n{}\n\nGathered Data:\n{}\n\nDraft Answer:\n{}",
@@ -139,6 +145,7 @@ impl AgentRouter {
139
145
  gathered_data,
140
146
  sandbox_path,
141
147
  tx.clone(),
148
+ None,
142
149
  )
143
150
  .await?;
144
151
  if let Some(u) = usage {
@@ -157,6 +164,8 @@ impl AgentRouter {
157
164
 
158
165
  let (temp_tx, mut temp_rx) = tokio::sync::mpsc::unbounded_channel::<String>();
159
166
  let real_tx_clone = tx.clone();
167
+ let write_buf: SharedWriteBuffer = Arc::new(WriteBuffer::new(Vec::new()));
168
+ let write_buf_clone = write_buf.clone();
160
169
 
161
170
  let collect_handle = tokio::spawn(async move {
162
171
  let mut accumulated_text = String::new();
@@ -174,7 +183,13 @@ impl AgentRouter {
174
183
  });
175
184
 
176
185
  let res = self
177
- .run_explain_agent_stream_internal(&messages, gathered_data, sandbox_path, temp_tx)
186
+ .run_explain_agent_stream_internal(
187
+ &messages,
188
+ gathered_data,
189
+ sandbox_path,
190
+ temp_tx,
191
+ Some(write_buf_clone),
192
+ )
178
193
  .await;
179
194
 
180
195
  let (accumulated_text, accumulated_reasoning) = match collect_handle.await {
@@ -192,12 +207,25 @@ impl AgentRouter {
192
207
 
193
208
  let _ = tx.send("\x01STATUS:Verify Agent: Reviewing draft...".to_string());
194
209
  match self
195
- .run_verify_agent(&user_question, gathered_data, &accumulated_text)
210
+ .run_verify_agent(
211
+ &user_question,
212
+ gathered_data,
213
+ &accumulated_text,
214
+ sandbox_path,
215
+ )
196
216
  .await
197
217
  {
198
218
  Ok((passed, feedback)) => {
199
219
  if passed {
200
220
  let _ = tx.send("\x01STATUS:Verify Agent: Validation passed!".to_string());
221
+
222
+ if let Ok(lock) = write_buf.lock() {
223
+ if let Err(e) = self.commit_write_buffer(&lock) {
224
+ let _ =
225
+ tx.send(format!("\x01STATUS:Failed to commit files: {}", e));
226
+ }
227
+ }
228
+
201
229
  if !accumulated_reasoning.is_empty() {
202
230
  let _ = tx.send(format!("\x01REASONING:{}", accumulated_reasoning));
203
231
  }
@@ -232,6 +260,13 @@ impl AgentRouter {
232
260
  }
233
261
  Err(e) => {
234
262
  let _ = tx.send(format!("\x01STATUS:Verify Agent error: {}, skipping...", e));
263
+
264
+ if let Ok(lock) = write_buf.lock() {
265
+ if let Err(e) = self.commit_write_buffer(&lock) {
266
+ let _ = tx.send(format!("\x01STATUS:Failed to commit files: {}", e));
267
+ }
268
+ }
269
+
235
270
  if !accumulated_reasoning.is_empty() {
236
271
  let _ = tx.send(format!("\x01REASONING:{}", accumulated_reasoning));
237
272
  }
@@ -268,6 +303,7 @@ impl AgentRouter {
268
303
  gathered_data: &str,
269
304
  sandbox_path: &Path,
270
305
  tx: tokio::sync::mpsc::UnboundedSender<String>,
306
+ write_buffer: Option<SharedWriteBuffer>,
271
307
  ) -> Result<(Option<Usage>, &'static str), String> {
272
308
  let (api_key, api_url, model_name_static) = if let Some(ref g) = self.glm {
273
309
  (
@@ -300,7 +336,7 @@ impl AgentRouter {
300
336
  .unwrap_or("")
301
337
  .to_string();
302
338
 
303
- let system_prompt = format!(
339
+ let mut system_prompt = format!(
304
340
  "You are COBOLX, an expert COBOL systems analyst and legacy migration specialist.\n\
305
341
  A retrieval agent has already gathered this structured data from the COBOL project:\n\n\
306
342
  ---\n{gathered_data}\n---\n\n\
@@ -308,9 +344,11 @@ impl AgentRouter {
308
344
  - Explanation/analysis: Markdown document covering program purpose, data structures, \
309
345
  business logic, CALL graph, COPY dependencies, and migration notes.\n\
310
346
  - Documentation (e.g. /docs): use write_file to write Markdown docs to docs/.\n\
347
+ CRITICAL: You MUST preserve the original base name of the COBOL source file and replace its extension with `.md` (e.g., write to `docs/XYZ.md` for a source file named `XYZ.cbl` or `XYZ.cob`). Do NOT use generic names like `entity.md` or `model.md` unless explicitly asked.\n\
311
348
  Sandbox root for write_file: {sandbox_display}",
312
349
  sandbox_display = sandbox_path.to_string_lossy()
313
350
  );
351
+ append_agent_skills(&mut system_prompt, sandbox_path, AgentKind::Explain)?;
314
352
 
315
353
  let mut messages = vec![
316
354
  ChatMessage {
@@ -455,21 +493,14 @@ impl AgentRouter {
455
493
  let path_str = args.get("path").and_then(|v| v.as_str()).unwrap_or("");
456
494
  let content = args.get("content").and_then(|v| v.as_str()).unwrap_or("");
457
495
  let _ = tx.send(format!("\x01STATUS:Writing file: {path_str}"));
458
- match Self::validate_sandbox_path(sandbox_path, path_str) {
496
+ match self.write_file(sandbox_path, path_str, content, write_buffer.as_deref())
497
+ {
498
+ Ok(full_path) => serde_json::json!({
499
+ "ok": true,
500
+ "path": full_path.to_string_lossy()
501
+ })
502
+ .to_string(),
459
503
  Err(e) => serde_json::json!({ "error": e }).to_string(),
460
- Ok(full_path) => {
461
- if let Some(parent) = full_path.parent() {
462
- let _ = std::fs::create_dir_all(parent);
463
- }
464
- match std::fs::write(&full_path, content) {
465
- Ok(_) => serde_json::json!({
466
- "ok": true,
467
- "path": full_path.to_string_lossy()
468
- })
469
- .to_string(),
470
- Err(e) => serde_json::json!({ "error": e.to_string() }).to_string(),
471
- }
472
- }
473
504
  }
474
505
  } else {
475
506
  serde_json::json!({ "error": format!("Unknown tool: {}", tc.function.name) })