feat: capture agent statistics via OTel and surface in safe outputs#219
Conversation
Create src/agent_stats.rs with AgentStats struct that parses Copilot CLI OpenTelemetry file exporter output. Extracts model, token usage, duration, tool calls, and turns from the last invoke_agent span and execute_tool span counts. Reuses ndjson::read_ndjson_file for JSONL parsing. Includes to_markdown() renderer with collapsible details block. 9 unit tests including real copilot-otel.jsonl fixture. Closes: part of #168 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add agent_name: Option<String> and agent_stats: Option<AgentStats> to ExecutionContext - Plumb front_matter.name to ctx.agent_name in main.rs execute handler - Load otel.jsonl from safe_output_dir, parse into ctx.agent_stats (non-fatal if missing — just logs debug/warn) - Add OTel env vars to base.yml AWF step (always-on: COPILOT_OTEL_ENABLED, COPILOT_OTEL_FILE_EXPORTER_PATH, COPILOT_OTEL_EXPORTER_TYPE) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good overall with a couple of things worth addressing before merge — one security gap and one misleading metric. Findings🔒 Security Concerns
🐛 Bugs / Logic Issues
|
Append a collapsible markdown stats block to safe outputs that produce
human-readable content. Each tool reads include_stats from its typed
config struct (deserialized via ctx.get_tool_config), matching the
existing config pattern used for all other tool options.
Safe outputs with stats:
- 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)
Per-tool opt-out via front matter:
safe-outputs:
create-pull-request:
include-stats: false
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
62008cd to
fbfb385
Compare
…ent_name Security: - Sanitize model and agent_name before embedding in markdown stats block. Strips control chars, neutralizes ##vso[ pipeline commands, escapes pipe chars that break markdown tables. The OTel file is writable by the agent inside AWF, so these values are untrusted. Bug fix: - Filter out Copilot CLI internal tool spans (report_intent, permission) from tool_calls count. Only user-visible tool invocations are counted. Cleanup: - Remove redundant agent_name from ExecutionContext — AgentStats already stores it. Single source of truth. Docs: - Document include-stats option for all 6 safe outputs in AGENTS.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Azure DevOps does not render HTML <details>/<summary> as collapsible sections — they display as raw text. Switch to a horizontal rule + italic heading + table, which renders correctly across ADO PRs, work items, and wiki pages. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace table with a single line using middle-dot separators: 🤖 _Agent Name_ · model · 45,230 in / 12,450 out · 23 tool calls · 4m 32s Remove turns from output (low value to operators). Remove unused total_tokens() method. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Mostly solid, but has a Findings🐛 Bugs / Logic Issues
|
…ooter order
Bugs fixed:
- CommentOnWorkItemConfig, CreateWikiPageConfig, UpdateWikiPageConfig:
replace #[derive(Default)] with manual Default impls so include_stats
defaults to true (not false from bool default)
- Remove dead "execute_tool permission" from INTERNAL_TOOL_NAMES — the
real OTel span is "permission" (no prefix), already excluded by the
starts_with("execute_tool") predicate
Improvements:
- Strip newlines in sanitize_for_markdown (single-line format)
- Reorder PR description: stats before provenance footer (footer is
the final unambiguous security marker)
- Add trailing newlines to add_pr_comment.rs and create_work_item.rs
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Good overall design and solid test coverage; one rendering bug with markdown escaping and a few minor issues worth cleaning up before merge. Findings🐛 Bugs / Logic Issues
let name = sanitize_for_markdown(&self.agent_name);
// ...
"\u{1F916} _{name}_ \u{00B7} {model} \u{00B7} ..."
Fix: add fn sanitize_for_markdown(s: &str) -> String {
s.chars()
.filter(|c| !c.is_control())
.collect::<String>()
.replace("##vso[", "[vso-filtered][")
.replace("##[", "[filtered][")
.replace('_', "\\_") // add
.replace('*', "\\*") // add
.replace('|', "\\|")
}(Alternatively, don't wrap
|
…s, tests - Remove italic underscores from agent name in stats line (plain text) - Fix orphaned doc comment on compute_duration - Consolidate 5 duplicate default_include_stats() fns into single pub(crate) fn in agent_stats.rs - Add 3 unit tests for append_stats_to_body (opt-out, no-stats, with-stats) - Fix trailing newlines Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good overall — clean design with appropriate error handling and good test coverage. A few observations worth noting. Findings🐛 Bugs / Logic Issues
|
Summary
Captures agent execution statistics (token usage, duration, model, tool calls, turns) using Copilot CLI's native OpenTelemetry file exporter and surfaces them in safe output write actions.
How it works
COPILOT_OTEL_FILE_EXPORTER_PATH=/tmp/awf-tools/staging/otel.jsonl(always-on, zero cost)ado-aw executeparses the OTel JSONL, populatesExecutionContext.agent_statsExample output
Safe outputs with stats
Each supports per-tool
include-stats: falseopt-out in front matter:create-pull-requestcreate-work-itemcomment-on-work-itemadd-pr-commentcreate-wiki-pageupdate-wiki-pageOpt-out example
Changes
src/agent_stats.rs(new)AgentStatsstruct, OTel JSONL parser (reusesndjson),to_markdown(),append_stats_to_body(), 9 unit testssrc/safeoutputs/result.rsagent_nameandagent_statstoExecutionContextsrc/main.rsfront_matter.nameand loadsotel.jsonlinto contexttemplates/base.ymlappend_stats_to_body()tests/fixtures/copilot-otel.jsonlCloses #168