Skip to content

Commit 62008cd

Browse files
jamesadevineCopilot
andcommitted
feat: append agent stats to 6 safe output write actions
Append a collapsible markdown stats block to safe outputs that produce human-readable content: - create-pull-request (PR description) - create-work-item (work item description) - comment-on-work-item (comment body) - add-pr-comment (PR comment body) - create-wiki-page (wiki page content) - update-wiki-page (wiki page content) Stats are appended when ctx.agent_stats is populated and the per-tool include-stats config is not explicitly false. Example output: <details> <summary>🤖 Agent Stats (Daily Code Review)</summary> | Model | claude-opus-4.5 | | Tokens | 45,230 input / 12,450 output | | Duration | 4m 32s | </details> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 41e5248 commit 62008cd

7 files changed

Lines changed: 64 additions & 7 deletions

File tree

src/agent_stats.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,32 @@ impl AgentStats {
147147
}
148148
}
149149

150-
/// Compute duration from OTel span startTime/endTime.
150+
/// Append agent stats markdown to a body string if stats are available
151+
/// and `include-stats` is not explicitly set to false in the tool config.
152+
///
153+
/// Used by safe output executors (create-pr, create-work-item, etc.)
154+
/// to append a collapsible stats block.
155+
pub fn append_stats_to_body(
156+
body: &str,
157+
ctx: &crate::safeoutputs::ExecutionContext,
158+
tool_name: &str,
159+
) -> String {
160+
let include = ctx
161+
.tool_configs
162+
.get(tool_name)
163+
.and_then(|v| v.get("include-stats"))
164+
.and_then(|v| v.as_bool())
165+
.unwrap_or(true);
166+
167+
if !include {
168+
return body.to_string();
169+
}
170+
171+
match &ctx.agent_stats {
172+
Some(stats) => format!("{}{}", body, stats.to_markdown()),
173+
None => body.to_string(),
174+
}
175+
}
151176
///
152177
/// Times are `[seconds, nanoseconds]` arrays.
153178
fn compute_duration(span: &Value) -> f64 {

src/safeoutputs/add_pr_comment.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,11 @@ impl Executor for AddPrCommentResult {
286286
Some(prefix) => format!("{}{}", prefix, self.content),
287287
None => self.content.clone(),
288288
};
289+
let comment_body = crate::agent_stats::append_stats_to_body(
290+
&comment_body,
291+
ctx,
292+
"add-pr-comment",
293+
);
289294

290295
// Build the API URL
291296
let url = format!(

src/safeoutputs/comment_on_work_item.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,13 @@ impl Executor for CommentOnWorkItemResult {
258258
);
259259
debug!("API URL: {}", url);
260260

261+
let body_with_stats = crate::agent_stats::append_stats_to_body(
262+
&self.body,
263+
ctx,
264+
"comment-on-work-item",
265+
);
261266
let comment_body = serde_json::json!({
262-
"text": self.body,
267+
"text": body_with_stats,
263268
});
264269

265270
info!("Sending comment to work item #{}", self.work_item_id);

src/safeoutputs/create_pr.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,8 +1255,13 @@ impl Executor for CreatePrResult {
12551255
}
12561256
debug!("Changes pushed successfully");
12571257

1258-
// Append provenance footer to description
1258+
// Append provenance footer and agent stats to description
12591259
let description_with_footer = format!("{}{}", self.description, generate_pr_footer());
1260+
let description_with_stats = crate::agent_stats::append_stats_to_body(
1261+
&description_with_footer,
1262+
ctx,
1263+
CreatePrResult::NAME,
1264+
);
12601265

12611266
// Create the pull request via REST API
12621267
info!("Creating pull request");
@@ -1270,7 +1275,7 @@ impl Executor for CreatePrResult {
12701275
"sourceRefName": source_ref,
12711276
"targetRefName": target_ref,
12721277
"title": effective_title,
1273-
"description": description_with_footer,
1278+
"description": description_with_stats,
12741279
"isDraft": config.draft,
12751280
});
12761281

src/safeoutputs/create_wiki_page.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,13 @@ impl Executor for CreateWikiPageResult {
321321
.header("Content-Type", "application/json")
322322
.header("If-Match", "")
323323
.basic_auth("", Some(token))
324-
.json(&serde_json::json!({ "content": self.content }))
324+
.json(&serde_json::json!({
325+
"content": crate::agent_stats::append_stats_to_body(
326+
&self.content,
327+
ctx,
328+
"create-wiki-page",
329+
)
330+
}))
325331
.send()
326332
.await
327333
.context("Failed to create wiki page")?;

src/safeoutputs/create_work_item.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,9 +269,14 @@ impl Executor for CreateWorkItemResult {
269269
debug!("API URL: {}", url);
270270

271271
// Build the patch document for work item creation
272+
let description_with_stats = crate::agent_stats::append_stats_to_body(
273+
&self.description,
274+
ctx,
275+
"create-work-item",
276+
);
272277
let mut patch_doc = vec![
273278
field_op("System.Title", &self.title),
274-
field_op("System.Description", &self.description),
279+
field_op("System.Description", &description_with_stats),
275280
// Tell Azure DevOps the description is markdown
276281
serde_json::json!({
277282
"op": "add",

src/safeoutputs/update_wiki_page.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,13 @@ impl Executor for UpdateWikiPageResult {
316316
.query(&put_query)
317317
.header("Content-Type", "application/json")
318318
.basic_auth("", Some(token))
319-
.json(&serde_json::json!({ "content": self.content }));
319+
.json(&serde_json::json!({
320+
"content": crate::agent_stats::append_stats_to_body(
321+
&self.content,
322+
ctx,
323+
"update-wiki-page",
324+
)
325+
}));
320326

321327
// Provide the ETag for optimistic concurrency when updating an existing page.
322328
if let Some(etag) = &etag {

0 commit comments

Comments
 (0)