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 +1 -1
- package/Cargo.toml +1 -1
- package/package.json +1 -1
- package/src/agent/client.rs +107 -8
- package/src/agent/db_agent.rs +71 -23
- package/src/agent/explain_agent.rs +53 -22
- package/src/agent/fs_agent.rs +211 -83
- package/src/agent/skills.rs +336 -0
- package/src/agent/types.rs +7 -0
- package/src/agent.rs +1 -0
- package/src/cobol/indexer.rs +375 -5
- package/src/cobol/model.rs +78 -0
- package/src/cobol/scanner.rs +2 -0
- package/src/cobol/source_parser.rs +341 -2
- package/src/lib.rs +1 -0
- package/src/main.rs +1 -0
- package/src/memory/memories.rs +208 -0
- package/src/memory/runs.rs +161 -0
- package/src/memory/store.rs +120 -0
- package/src/memory.rs +8 -2
- package/src/path_safety.rs +280 -0
- package/src/ui/draw.rs +1 -0
- package/src/ui/tui.rs +239 -0
- package/tests/indexer_tests.rs +261 -0
- package/tests/project_files_tests.rs +23 -51
- package/src/memory/files.rs +0 -155
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
package/package.json
CHANGED
package/src/agent/client.rs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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::*;
|
package/src/agent/db_agent.rs
CHANGED
|
@@ -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
|
-
|
|
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\
|
|
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
|
|
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.
|
|
189
|
-
Ok(
|
|
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,
|
|
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
|
|
35
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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) })
|