1use std::path::PathBuf;
11use std::sync::Arc;
12
13use crate::agent::capability::Capability;
14use crate::agent::driver::LlmDriver;
15use crate::agent::manifest::{AgentManifest, ModelConfig, ResourceQuota};
16use crate::agent::tool::file::{FileEditTool, FileReadTool, FileWriteTool};
17use crate::agent::tool::search::{GlobTool, GrepTool};
18use crate::agent::tool::shell::ShellTool;
19use crate::agent::tool::ToolRegistry;
20use crate::serve::backends::PrivacyTier;
21
22#[allow(clippy::too_many_arguments)]
28pub fn cmd_code(
29 model: Option<PathBuf>,
30 project: PathBuf,
31 resume: Option<Option<String>>,
32 prompt: Vec<String>,
33 print: bool,
34 max_turns: u32,
35 manifest_path: Option<PathBuf>,
36 emit_trace: Option<PathBuf>,
37 output_format: &str,
42 input_format: &str,
43) -> anyhow::Result<()> {
44 if project.as_os_str() != "." && project.is_dir() {
46 std::env::set_current_dir(&project)?;
47 }
48
49 let mut manifest = match manifest_path {
56 Some(ref path) => {
57 let content = std::fs::read_to_string(path)
58 .map_err(|e| anyhow::anyhow!("cannot read manifest {}: {e}", path.display()))?;
59 let m = AgentManifest::from_toml(&content)
60 .map_err(|e| anyhow::anyhow!("invalid manifest: {e}"))?;
61 eprintln!("✓ Loaded manifest: {}", path.display());
62 m
63 }
64 None => {
65 let mut m = build_default_manifest();
66 let project_root = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
70 let settings = crate::agent::settings::AprSettings::load_layered(&project_root)?;
71 apply_settings_to_manifest(&mut m, &settings)?;
72 m
73 }
74 };
75
76 if let Some(ref model_path) = model {
80 manifest.model.model_path = Some(model_path.clone());
81 }
82
83 discover_and_set_model(&mut manifest);
85
86 if let Some(ref path) = manifest.model.model_path {
89 let params_b = estimate_model_params_from_name(path);
90 if params_b < 2.0 {
91 manifest.model.system_prompt = scale_prompt_for_model(params_b);
92 }
93 }
94
95 if manifest.model.resolve_model_path().is_none() && manifest_path.is_none() {
97 print_no_model_error();
98 std::process::exit(exit_code::NO_MODEL);
99 }
100
101 let driver: Arc<dyn LlmDriver> = if let Some(model_path) = manifest.model.resolve_model_path() {
106 match crate::agent::driver::apr_serve::AprServeDriver::launch(
107 model_path,
108 manifest.model.context_window,
109 ) {
110 Ok(d) => Arc::new(d),
111 Err(e) => {
112 eprintln!("⚠ apr serve unavailable ({e}), using embedded inference");
113 Arc::from(build_fallback_driver(&manifest)?)
114 }
115 }
116 } else {
117 Arc::from(build_fallback_driver(&manifest)?)
118 };
119
120 #[cfg(feature = "agents-mcp")]
126 {
127 let project_root = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
128 match crate::agent::mcp_json::load_and_merge(&mut manifest, &project_root) {
129 Ok(0) => {}
130 Ok(n) => {
131 eprintln!("✓ Loaded {n} MCP server(s) from .mcp.json");
132 }
133 Err(e) => {
134 anyhow::bail!("invalid .mcp.json: {e}");
135 }
136 }
137 }
138
139 let mut tools = build_code_tools(&manifest);
141
142 register_mcp_client_tools(&mut tools, &manifest);
146
147 crate::agent::task_tool::register_task_tool(
151 &mut tools,
152 &manifest,
153 Arc::clone(&driver),
154 3,
155 );
156
157 let hooks_reg = crate::agent::hooks::HookRegistry::from_configs(manifest.hooks.clone());
161 let hook_cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
162 match hooks_reg.run(crate::agent::hooks::HookEvent::SessionStart, "", &hook_cwd) {
163 crate::agent::hooks::HookDecision::Allow => {}
164 crate::agent::hooks::HookDecision::Warn(msg) => {
165 if !msg.is_empty() {
166 eprintln!("⚠ SessionStart hook: {msg}");
167 }
168 }
169 crate::agent::hooks::HookDecision::Block(reason) => {
170 anyhow::bail!("SessionStart hook blocked session: {reason}");
171 }
172 }
173
174 let memory = crate::agent::memory::InMemorySubstrate::new();
176
177 if print || !prompt.is_empty() {
181 let prompt_text = if prompt.is_empty() {
182 let mut buf = String::new();
183 std::io::Read::read_to_string(&mut std::io::stdin(), &mut buf)?;
184 if input_format.eq_ignore_ascii_case("json") {
189 parse_json_input_envelope(&buf)?
190 } else {
191 buf
192 }
193 } else {
194 prompt.join(" ")
195 };
196 let code = run_single_prompt(
197 &manifest,
198 driver.as_ref(),
199 &tools,
200 &memory,
201 &prompt_text,
202 emit_trace.as_deref(),
203 output_format,
204 );
205 drop(driver); std::process::exit(code);
207 }
208
209 let resume_session_id = match resume {
212 Some(Some(id)) => Some(id), Some(None) => {
214 crate::agent::session::SessionStore::find_recent_for_cwd().map(|m| m.id)
216 }
217 None => {
218 crate::agent::session::offer_auto_resume()
220 }
221 };
222
223 crate::agent::repl::run_repl(
225 &manifest,
226 driver.as_ref(),
227 &tools,
228 &memory,
229 max_turns,
230 f64::MAX,
231 resume_session_id.as_deref(),
232 )
233}
234
235fn apply_settings_to_manifest(
247 manifest: &mut AgentManifest,
248 settings: &crate::agent::settings::AprSettings,
249) -> anyhow::Result<()> {
250 if let Some(ref model) = settings.model {
251 if std::path::Path::new(model).is_absolute()
255 || model.starts_with("./")
256 || model.starts_with("../")
257 || (!model.contains(':') && !model.starts_with("hf://"))
258 {
259 manifest.model.model_path = Some(std::path::PathBuf::from(model));
260 } else {
261 manifest.model.model_repo = Some(model.clone());
262 }
263 }
264 if let Some(extra) = settings.extra_system_prompt.as_deref() {
265 if !extra.trim().is_empty() {
266 manifest.model.system_prompt.push_str("\n\n");
269 manifest.model.system_prompt.push_str(extra);
270 }
271 }
272 if let Some(mt) = settings.max_turns {
273 manifest.resources.max_iterations = mt;
274 }
275 if let Some(ref pm) = settings.permission_mode {
276 if crate::agent::permission::PermissionMode::parse(pm).is_none() {
281 anyhow::bail!(
282 "settings.json permissionMode: unknown mode {pm:?} \
283 (expected default | plan | acceptEdits | bypassPermissions)"
284 );
285 }
286 }
287 if let Some(ref hosts) = settings.allowed_hosts {
288 if manifest.allowed_hosts.is_empty() {
291 manifest.allowed_hosts = hosts.clone();
292 }
293 }
294 Ok(())
295}
296
297fn build_fallback_driver(manifest: &AgentManifest) -> anyhow::Result<Box<dyn LlmDriver>> {
299 #[cfg(feature = "inference")]
300 {
301 if let Some(model_path) = manifest.model.resolve_model_path() {
302 let driver = crate::agent::driver::realizar::RealizarDriver::new(
303 model_path,
304 manifest.model.context_window,
305 )?;
306 return Ok(Box::new(driver));
307 }
308 }
309 let _ = manifest;
310 Ok(Box::new(crate::agent::driver::mock::MockDriver::single_response(
312 "Hello! I'm running in dry-run mode. \
313 Set model_path in your agent manifest or install the `apr` binary.",
314 )))
315}
316
317fn discover_and_set_model(manifest: &mut AgentManifest) {
319 if manifest.model.model_path.is_some() || manifest.model.model_repo.is_some() {
320 return;
321 }
322 let Some(discovered) = ModelConfig::discover_model() else {
323 return;
324 };
325 eprintln!(
326 "Model: {} (auto-discovered)",
327 discovered.file_name().unwrap_or_default().to_string_lossy()
328 );
329 let ext = discovered.extension().and_then(|e| e.to_str()).unwrap_or("");
330 if ext == "gguf" && check_invalid_apr_in_search_dirs() {
331 eprintln!(
332 "⚠ APR model found but invalid (missing tokenizer). Using GGUF fallback: {}",
333 discovered.display()
334 );
335 eprintln!(" Re-convert with: apr convert <source>.gguf -o <output>.apr\n");
336 }
337 manifest.model.model_path = Some(discovered);
338}
339
340fn print_no_model_error() {
342 eprintln!("✗ No local model found. apr code requires a local model.\n");
343 if check_invalid_apr_in_search_dirs() {
344 eprintln!(" ⚠ APR model(s) found but invalid (missing embedded tokenizer).");
345 eprintln!(" Re-convert: apr convert <source>.gguf -o <output>.apr\n");
346 }
347 eprintln!(" Download a model (APR format preferred):");
348 eprintln!(" apr pull qwen3:1.7b-q4k (default — best tool use at 1.2GB)");
349 eprintln!(" apr pull qwen3:8b-q4k (recommended for complex tasks)");
350 eprintln!();
351 eprintln!(" Or place a .apr/.gguf file in ~/.apr/models/ (auto-discovered)");
352 eprintln!();
353 eprintln!(" Then run: apr code or apr code --model <path>");
354}
355
356fn check_invalid_apr_in_search_dirs() -> bool {
358 for dir in &ModelConfig::model_search_dirs() {
359 if let Ok(entries) = std::fs::read_dir(dir) {
360 for entry in entries.flatten() {
361 let path = entry.path();
362 if path.extension().is_some_and(|e| e == "apr")
363 && !crate::agent::driver::validate::is_valid_model_file(&path)
364 {
365 return true;
366 }
367 }
368 }
369 }
370 false
371}
372
373fn load_project_instructions(max_bytes: usize) -> Option<String> {
375 let cwd = std::env::current_dir().ok()?;
376
377 for filename in &["APR.md", "CLAUDE.md"] {
378 let path = cwd.join(filename);
379 if path.is_file() {
380 if let Ok(content) = std::fs::read_to_string(&path) {
381 if max_bytes == 0 {
382 return None;
383 }
384 let truncated = if content.len() > max_bytes {
385 let end = content
386 .char_indices()
387 .take_while(|(i, _)| *i < max_bytes)
388 .last()
389 .map(|(i, c)| i + c.len_utf8())
390 .unwrap_or(max_bytes.min(content.len()));
391 format!("{}...\n(truncated from {} bytes)", &content[..end], content.len())
392 } else {
393 content
394 };
395 return Some(truncated);
396 }
397 }
398 }
399 None
400}
401
402fn instruction_budget(context_window: usize) -> usize {
404 if context_window < 4096 {
405 return 0;
406 }
407 let budget = context_window / 4;
408 budget.min(4096)
409}
410
411fn assemble_system_prompt(
432 base: &str,
433 project_context: &str,
434 project_instructions: Option<&str>,
435 auto_memory: Option<&str>,
436 org_policy: Option<&crate::agent::org_policy::OrgPolicy>,
437) -> String {
438 let mut out = String::from(base);
439 if let Some(pol) = org_policy {
440 out.push_str(&format!(
441 "\n\n## Enforced organization policy ({source})\n\n{content}",
442 source = pol.source.display(),
443 content = pol.content
444 ));
445 }
446 out.push_str(&format!("\n\n## Project Context\n\n{project_context}"));
447 if let Some(instructions) = project_instructions {
448 out.push_str(&format!("\n## Project Instructions\n\n{instructions}"));
449 }
450 if let Some(mem) = auto_memory {
451 out.push_str(&format!("\n## Auto-memory\n\n{mem}"));
452 }
453 out
454}
455
456fn gather_project_context() -> String {
458 let mut ctx = String::new();
459 let cwd = std::env::current_dir().unwrap_or_default();
460 ctx.push_str(&format!("Working directory: {}\n", cwd.display()));
461
462 if let Ok(output) =
463 std::process::Command::new("git").args(["rev-parse", "--abbrev-ref", "HEAD"]).output()
464 {
465 if output.status.success() {
466 let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
467 ctx.push_str(&format!("Git branch: {branch}\n"));
468 }
469 }
470 if let Ok(output) =
471 std::process::Command::new("git").args(["diff", "--stat", "--no-color"]).output()
472 {
473 if output.status.success() {
474 let diff = String::from_utf8_lossy(&output.stdout);
475 let dirty_count = diff.lines().count().saturating_sub(1);
476 if dirty_count > 0 {
477 ctx.push_str(&format!("Dirty files: {dirty_count}\n"));
478 }
479 }
480 }
481
482 let mut rs_count = 0u32;
483 let mut py_count = 0u32;
484 let mut total = 0u32;
485 if let Ok(entries) = std::fs::read_dir("src") {
486 for e in entries.flatten() {
487 total += 1;
488 if let Some(ext) = e.path().extension() {
489 match ext.to_str() {
490 Some("rs") => rs_count += 1,
491 Some("py") => py_count += 1,
492 _ => {}
493 }
494 }
495 }
496 }
497 let lang = if rs_count > py_count {
498 "Rust"
499 } else if py_count > 0 {
500 "Python"
501 } else {
502 "unknown"
503 };
504 ctx.push_str(&format!("Language: {lang} ({total} files in src/)\n"));
505
506 if PathBuf::from("Cargo.toml").exists() {
507 ctx.push_str("Build system: Cargo (Rust)\n");
508 } else if PathBuf::from("pyproject.toml").exists() {
509 ctx.push_str("Build system: pyproject.toml (Python)\n");
510 }
511
512 ctx
513}
514
515fn build_default_manifest() -> AgentManifest {
517 let ctx_window = 4096_usize;
518 let budget = instruction_budget(ctx_window);
519 let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
523 let mut import_warnings = Vec::new();
524 let project_instructions =
525 crate::agent::instructions::load_layered_instructions(&cwd, budget, &mut import_warnings)
526 .or_else(|| load_project_instructions(budget));
527 for w in &import_warnings {
528 eprintln!("⚠ instructions: {w}");
529 }
530 let project_context = gather_project_context();
531
532 let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
538 let mut auto_warns: Vec<String> = Vec::new();
539 let auto_memory = crate::agent::auto_memory::load_auto_memory(&cwd, &mut auto_warns);
540 for w in &auto_warns {
541 eprintln!("⚠ {w}");
542 }
543
544 let org_policy = crate::agent::org_policy::load_org_policy(
553 &crate::agent::org_policy::canonical_system_roots(),
554 "CLAUDE.md",
555 budget,
556 );
557
558 let system_prompt = assemble_system_prompt(
559 CODE_SYSTEM_PROMPT,
560 &project_context,
561 project_instructions.as_deref(),
562 auto_memory.as_deref(),
563 org_policy.as_ref(),
564 );
565
566 AgentManifest {
567 name: "apr-code".to_string(),
568 description: "Interactive AI coding assistant".to_string(),
569 privacy: PrivacyTier::Sovereign,
570 model: ModelConfig {
571 system_prompt,
572 max_tokens: 4096,
573 temperature: 0.0,
574 context_window: Some(32768),
578 ..ModelConfig::default()
579 },
580 resources: ResourceQuota {
581 max_iterations: 50,
582 max_tool_calls: 200,
583 max_cost_usd: 0.0,
584 max_tokens_budget: None,
585 },
586 capabilities: vec![
587 Capability::FileRead { allowed_paths: vec!["*".into()] },
588 Capability::FileWrite { allowed_paths: vec!["*".into()] },
589 Capability::Shell { allowed_commands: vec!["*".into()] },
590 Capability::Memory,
591 Capability::Rag,
592 ],
593 ..AgentManifest::default()
594 }
595}
596
597#[allow(unused_variables)]
604fn register_mcp_client_tools(tools: &mut ToolRegistry, manifest: &AgentManifest) {
605 #[cfg(feature = "agents-mcp")]
606 {
607 if manifest.mcp_servers.is_empty() {
608 return;
609 }
610 let rt = match tokio::runtime::Builder::new_current_thread().enable_all().build() {
611 Ok(rt) => rt,
612 Err(e) => {
613 eprintln!("⚠ failed to create MCP discovery runtime: {e}");
614 return;
615 }
616 };
617 let discovered = rt.block_on(crate::agent::tool::mcp_client::discover_mcp_tools(manifest));
618 let count = discovered.len();
619 for tool in discovered {
620 tools.register(Box::new(tool));
621 }
622 if count > 0 {
623 eprintln!(
624 "✓ Registered {count} MCP tool(s) from {} server(s)",
625 manifest.mcp_servers.len()
626 );
627 }
628 }
629}
630
631fn build_code_tools(manifest: &AgentManifest) -> ToolRegistry {
633 let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
634
635 let mut tools = ToolRegistry::new();
636 tools.register(Box::new(FileReadTool::new(vec!["*".into()])));
637 tools.register(Box::new(FileWriteTool::new(vec!["*".into()])));
638 tools.register(Box::new(FileEditTool::new(vec!["*".into()])));
639 tools.register(Box::new(GlobTool::new(vec!["*".into()])));
640 tools.register(Box::new(GrepTool::new(vec!["*".into()])));
641 tools.register(Box::new(ShellTool::new(vec!["*".into()], cwd)));
642
643 let memory_sub = Arc::new(crate::agent::memory::InMemorySubstrate::new());
644 tools.register(Box::new(crate::agent::tool::memory::MemoryTool::new(
645 memory_sub,
646 manifest.name.clone(),
647 )));
648
649 tools.register(Box::new(crate::agent::tool::pmat_query::PmatQueryTool::new()));
651
652 #[cfg(feature = "rag")]
653 {
654 let oracle = Arc::new(crate::oracle::rag::RagOracle::new());
655 tools.register(Box::new(crate::agent::tool::rag::RagTool::new(oracle, 5)));
656 }
657
658 register_web_tools(&mut tools, manifest);
662
663 tools
664}
665
666fn register_web_tools(tools: &mut ToolRegistry, manifest: &AgentManifest) {
670 use crate::serve::backends::PrivacyTier;
671
672 if matches!(manifest.privacy, PrivacyTier::Sovereign) {
673 return;
674 }
675 if manifest.allowed_hosts.is_empty() {
676 return;
677 }
678
679 tools.register(Box::new(crate::agent::tool::network::NetworkTool::new(
680 manifest.allowed_hosts.clone(),
681 )));
682
683 #[cfg(feature = "agents-browser")]
684 {
685 tools.register(Box::new(crate::agent::tool::browser::BrowserTool::new(manifest.privacy)));
686 }
687}
688
689pub use super::code_prompts::exit_code;
690
691fn run_single_prompt(
693 manifest: &AgentManifest,
694 driver: &dyn LlmDriver,
695 tools: &ToolRegistry,
696 memory: &dyn crate::agent::memory::MemorySubstrate,
697 prompt: &str,
698 emit_trace: Option<&std::path::Path>,
699 output_format: &str,
701) -> i32 {
702 let mut single_manifest = manifest.clone();
703 single_manifest.resources.max_iterations = single_manifest.resources.max_iterations.min(10);
704 single_manifest.model.system_prompt = COMPACT_SYSTEM_PROMPT.to_string();
709 let rt = match tokio::runtime::Builder::new_current_thread().enable_all().build() {
713 Ok(rt) => rt,
714 Err(e) => {
715 eprintln!("Error: failed to create tokio runtime: {e}");
716 return exit_code::AGENT_ERROR;
717 }
718 };
719
720 let started = std::time::Instant::now();
721
722 let result = rt.block_on(crate::agent::runtime::run_agent_loop(
726 &single_manifest,
727 prompt,
728 driver,
729 tools,
730 memory,
731 None,
732 ));
733
734 match result {
735 Ok(r) => {
736 let elapsed = started.elapsed();
737 if r.text.is_empty() {
738 eprintln!(
742 "⚠ Empty response ({} iterations, {} tool calls). \
743 Model may be in thinking mode — rebuild apr from source for Qwen3NoThinkTemplate fix.",
744 r.iterations, r.tool_calls
745 );
746 if output_format.eq_ignore_ascii_case("json") {
747 println!("{}", build_json_result_envelope(&r, elapsed, true));
748 }
749 } else if output_format.eq_ignore_ascii_case("json") {
750 println!("{}", build_json_result_envelope(&r, elapsed, false));
753 } else {
754 println!("{}", r.text);
755 }
756
757 if let Some(trace_path) = emit_trace {
761 let model = single_manifest
762 .model
763 .resolve_model_path()
764 .map(|p| p.display().to_string())
765 .unwrap_or_else(|| "apr-code-unknown".to_owned());
766 if let Err(e) = emit_ccpa_trace(trace_path, prompt, &r, started.elapsed(), &model) {
767 eprintln!("⚠ failed to write ccpa-trace to {}: {e}", trace_path.display());
768 }
769 }
770
771 exit_code::SUCCESS
772 }
773 Err(e) => {
774 eprintln!("Error: {e}");
775 map_error_to_exit_code(&e)
776 }
777 }
778}
779
780fn emit_ccpa_trace(
795 path: &std::path::Path,
796 prompt: &str,
797 result: &super::result::AgentLoopResult,
798 elapsed: std::time::Duration,
799 model: &str,
800) -> std::io::Result<()> {
801 use std::time::{SystemTime, UNIX_EPOCH};
802
803 let ts_micros =
804 SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_micros()).unwrap_or(0);
805 let session_id = format!(
810 "{:08x}-{:04x}-7000-{:04x}-{:012x}",
811 (ts_micros >> 64) as u32 & 0xFFFF_FFFF,
812 ((ts_micros >> 48) & 0xFFFF) as u16,
813 ((ts_micros >> 32) & 0xFFFF) as u16,
814 (ts_micros & 0xFFFF_FFFF_FFFF) as u64
815 );
816 let secs = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
819 let ts = format!("@{secs}");
820 let cwd_sha256 = "0".repeat(64);
821
822 let session_start = serde_json::json!({
823 "v": 1,
824 "kind": "session_start",
825 "session_id": session_id,
826 "ts": ts,
827 "actor": "apr-code",
828 "model": model,
829 "cwd_sha256": cwd_sha256,
830 });
831 let user_prompt = serde_json::json!({
832 "v": 1,
833 "kind": "user_prompt",
834 "turn": 0,
835 "text": prompt,
836 });
837 let assistant_turn = serde_json::json!({
838 "v": 1,
839 "kind": "assistant_turn",
840 "turn": 1,
841 "blocks": [{"type": "text", "text": result.text}],
842 "stop_reason": "end_turn",
843 });
844 let session_end = serde_json::json!({
845 "v": 1,
846 "kind": "session_end",
847 "turn": 1,
848 "stop_reason": "end_turn",
849 "elapsed_ms": elapsed.as_millis() as u64,
850 "tokens_in": result.usage.input_tokens,
851 "tokens_out": result.usage.output_tokens,
852 });
853
854 let body = format!("{}\n{}\n{}\n{}\n", session_start, user_prompt, assistant_turn, session_end);
855 std::fs::write(path, body)
856}
857
858fn parse_json_input_envelope(buf: &str) -> anyhow::Result<String> {
866 let trimmed = buf.trim();
867 if trimmed.is_empty() {
868 anyhow::bail!("--input-format=json: stdin is empty (expected JSON envelope)");
869 }
870 let v: serde_json::Value = serde_json::from_str(trimmed)
871 .map_err(|e| anyhow::anyhow!("--input-format=json: invalid JSON on stdin: {e}"))?;
872 let role = v.get("role").and_then(|r| r.as_str()).unwrap_or("user");
873 if role != "user" {
874 anyhow::bail!("--input-format=json: only role=\"user\" supported, got \"{role}\"");
875 }
876 let content = v
877 .get("content")
878 .and_then(|c| c.as_str())
879 .ok_or_else(|| anyhow::anyhow!("--input-format=json: missing string field `content`"))?;
880 Ok(content.to_owned())
881}
882
883fn build_json_result_envelope(
899 result: &super::result::AgentLoopResult,
900 elapsed: std::time::Duration,
901 is_error: bool,
902) -> String {
903 use std::time::{SystemTime, UNIX_EPOCH};
904 let ts_micros =
905 SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_micros()).unwrap_or(0);
906 let session_id = format!(
908 "{:08x}-{:04x}-7000-{:04x}-{:012x}",
909 (ts_micros >> 64) as u32 & 0xFFFF_FFFF,
910 ((ts_micros >> 48) & 0xFFFF) as u16,
911 ((ts_micros >> 32) & 0xFFFF) as u16,
912 (ts_micros & 0xFFFF_FFFF_FFFF) as u64
913 );
914 let envelope = serde_json::json!({
915 "type": "result",
916 "subtype": if is_error { "error" } else { "success" },
917 "is_error": is_error,
918 "duration_ms": elapsed.as_millis() as u64,
919 "result": result.text,
920 "session_id": session_id,
921 "num_turns": result.iterations,
922 "tokens_in": result.usage.input_tokens,
923 "tokens_out": result.usage.output_tokens,
924 "total_cost_usd": 0,
926 });
927 envelope.to_string()
928}
929
930use super::code_prompts::{
932 estimate_model_params_from_name, map_error_to_exit_code, scale_prompt_for_model,
933 CODE_SYSTEM_PROMPT, COMPACT_SYSTEM_PROMPT,
934};
935
936#[cfg(test)]
937#[path = "code_tests.rs"]
938mod tests;