返回 CodeWhale
report.rs
根目录 / crates / tui / src / tools / workflow / report.rs
1 use super::{SCHEMA_RAW_PREVIEW_CHARS, WorkflowRunRecord, WorkflowRunStatus};
2 use std::path::Path;
3
4 /// Persist a durable per-run report under `.codewhale/reports/<run_id>.md`
5 /// so a settled background run leaves one synthesized artifact even after
6 /// the session ends. Best-effort: report IO never affects the run outcome.
7 pub(super) fn write_run_report_artifact(workspace: &Path, record: &WorkflowRunRecord) {
8 if !matches!(
9 record.status,
10 WorkflowRunStatus::Completed
11 | WorkflowRunStatus::Degraded
12 | WorkflowRunStatus::Failed
13 | WorkflowRunStatus::Cancelled
14 ) {
15 return;
16 }
17 // Run ids are generated slugs, but never trust one as a path segment.
18 let safe_id: String = record
19 .run_id
20 .chars()
21 .filter(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_'))
22 .collect();
23 if safe_id.is_empty() {
24 return;
25 }
26 let dir = workspace.join(".codewhale").join("reports");
27 if let Err(err) = std::fs::create_dir_all(&dir) {
28 crate::logging::warn(format!(
29 "workflow report dir {} not created: {err}",
30 dir.display()
31 ));
32 return;
33 }
34 let path = dir.join(format!("{safe_id}.md"));
35 if let Err(err) = std::fs::write(&path, render_run_report(record)) {
36 crate::logging::warn(format!(
37 "workflow report {} not written: {err}",
38 path.display()
39 ));
40 }
41 }
42
43 /// Bounded preview of a raw `responseSchema` reply for run records and
44 /// reports (#5583): the first [`SCHEMA_RAW_PREVIEW_CHARS`] chars on a char
45 /// boundary, with an explicit marker when the text is longer.
46 pub(super) fn bounded_raw_preview(raw: &str) -> String {
47 if raw.chars().count() <= SCHEMA_RAW_PREVIEW_CHARS {
48 return raw.to_string();
49 }
50 let kept: String = raw.chars().take(SCHEMA_RAW_PREVIEW_CHARS).collect();
51 format!("{kept}\n…[preview truncated; full reply in the schema artifact]")
52 }
53
54 /// Write the full raw reply of a failed `responseSchema` attempt as a
55 /// durable artifact beside the run report (#5583), returning its path.
56 /// `None` (with a warning) when the write fails — the bounded preview
57 /// remains either way.
58 pub(super) fn write_schema_raw_artifact(
59 workspace: &Path,
60 run_id: &str,
61 task_id: &str,
62 attempt: u32,
63 raw: &str,
64 ) -> Option<String> {
65 // Run/task ids are generated slugs, but never trust one as a path segment.
66 let safe = |text: &str| {
67 text.chars()
68 .filter(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_'))
69 .collect::<String>()
70 };
71 let (safe_run, safe_task) = (safe(run_id), safe(task_id));
72 if safe_run.is_empty() || safe_task.is_empty() {
73 return None;
74 }
75 let dir = workspace.join(".codewhale").join("reports");
76 if let Err(err) = std::fs::create_dir_all(&dir) {
77 crate::logging::warn(format!(
78 "workflow schema artifact dir {} not created: {err}",
79 dir.display()
80 ));
81 return None;
82 }
83 let path = dir.join(format!(
84 "{safe_run}.schema.{safe_task}.attempt{attempt}.txt"
85 ));
86 match std::fs::write(&path, raw) {
87 Ok(()) => Some(path.display().to_string()),
88 Err(err) => {
89 crate::logging::warn(format!(
90 "workflow schema artifact {} not written: {err}",
91 path.display()
92 ));
93 None
94 }
95 }
96 }
97
98 pub(super) fn render_run_report(record: &WorkflowRunRecord) -> String {
99 let mut out = String::new();
100 out.push_str(&format!("# Workflow run {}\n\n", record.run_id));
101 out.push_str(&format!("- status: {:?}\n", record.status));
102 if let Some(goal) = record.workflow_goal.as_deref() {
103 out.push_str(&format!("- goal: {goal}\n"));
104 }
105 if let Some(source) = record.source_path.as_deref() {
106 out.push_str(&format!("- source: {}\n", source.display()));
107 }
108 out.push_str(&format!("- started_at_ms: {}\n", record.started_at_ms));
109 if let Some(completed) = record.completed_at_ms {
110 out.push_str(&format!("- completed_at_ms: {completed}\n"));
111 }
112 if let Some(budget) = record.token_budget {
113 out.push_str(&format!("- token_budget: {budget}\n"));
114 }
115 out.push_str(&format!("- child_agents: {}\n", record.child_ids.len()));
116 if let Some(error) = record.error.as_deref() {
117 out.push_str(&format!("- error: {error}\n"));
118 }
119 if record.dispatch_failure_count > 0 {
120 out.push_str(&format!(
121 "\n## Dispatch failures ({})\n\n",
122 record.dispatch_failure_count
123 ));
124 let omitted = record
125 .dispatch_failure_count
126 .saturating_sub(u64::try_from(record.dispatch_failures.len()).unwrap_or(u64::MAX));
127 if omitted > 0 {
128 out.push_str(&format!(
129 "- {omitted} older failure receipt(s) omitted from this bounded report; see the workflow journal\n"
130 ));
131 }
132 for failure in &record.dispatch_failures {
133 let slot = failure
134 .label
135 .as_deref()
136 .or(failure.phase.as_deref())
137 .unwrap_or("task");
138 out.push_str(&format!("- {slot}: {}\n", failure.message));
139 }
140 }
141 if !record.gate_status.is_empty() {
142 out.push_str("\n## Gates\n\n");
143 for line in &record.gate_status {
144 out.push_str(&format!("- {line:?}\n"));
145 }
146 }
147 if !record.progress.is_empty() {
148 out.push_str("\n## Progress\n\n");
149 for line in &record.progress {
150 out.push_str(&format!("- {line}\n"));
151 }
152 }
153 if !record.schema_errors.is_empty() {
154 out.push_str(&format!(
155 "\n## Schema errors ({})\n\n",
156 record.schema_errors.len()
157 ));
158 for error in &record.schema_errors {
159 out.push_str(&format!(
160 "- `{}` attempt {}: [{}] {}\n",
161 error.task_id, error.attempt, error.kind, error.message
162 ));
163 if !error.raw_preview.is_empty() {
164 out.push_str(&format!(
165 " - raw reply ({}):\n",
166 if error.raw_truncated {
167 "bounded preview; carried text was capped"
168 } else {
169 "bounded preview"
170 }
171 ));
172 for line in error.raw_preview.lines() {
173 out.push_str(&format!(" {line}\n"));
174 }
175 }
176 if let Some(artifact) = &error.artifact {
177 out.push_str(&format!(" - full reply: {artifact}\n"));
178 }
179 }
180 }
181 if !record.schema_repairs.is_empty() {
182 out.push_str(&format!(
183 "\n## Schema repairs ({}, including succeeded ones)\n\n",
184 record.schema_repair_count
185 ));
186 for repair in &record.schema_repairs {
187 out.push_str(&format!(
188 "- `{}` attempt {}: [{}] a bounded repair followed\n",
189 repair.task_id, repair.attempt, repair.kind
190 ));
191 if let Some(artifact) = &repair.artifact {
192 out.push_str(&format!(" - full reply: {artifact}\n"));
193 }
194 }
195 }
196 if let Some(result) = record.result.as_ref() {
197 out.push_str("\n## Result\n\n```json\n");
198 out.push_str(&serde_json::to_string_pretty(result).unwrap_or_else(|_| result.to_string()));
199 out.push_str("\n```\n");
200 }
201 if let Some(verification) = record.verification.as_ref() {
202 out.push_str("\n## Verification\n\n```json\n");
203 out.push_str(
204 &serde_json::to_string_pretty(verification)
205 .unwrap_or_else(|_| verification.to_string()),
206 );
207 out.push_str("\n```\n");
208 }
209 out
210 }
211
211 lines RUST