feat(governance,metrics): live cost attribution + pre-call budget projection (feat-026)#31
Merged
Merged
Conversation
…jection (feat-026) The live + pre-call delta over what already ships — both config over existing surfaces, no agent-code change. Live attributed-cost metrics (forgesight-core): - forgesight.cost.attributed_usd counter keyed by the stamped ownership dimensions (team/owner/…), the live counterpart of feat-022's offline chargeback rollup. A record missing a dimension buckets to <unattributed>. - forgesight.cost.budget_utilization gauge (spend/cap). The instrument is a generic gauge in core; the budget semantics stay in governance, which records through the runtime's metrics — core never depends on governance (P1). - AttributionMetricsConfig on MetricConfig; default off (P2). Pre-call budget projection (forgesight-governance): - BudgetInterceptor gains a projection mode + ProjectionConfig. A new precall() estimates the call's cost from caller-declared projected_tokens via the same PricingProvider and denies BEFORE the spend if a cap would breach — closing feat-020's post-hoc gap. It is a GUARD: it never commits to the running totals (actual cost on the completed record stays the sole writer). - output_token_estimate max_tokens|input_ratio|fixed; on_unpriced allow|deny. Minimal core hook: Runtime.run_precall_interceptors runs only interceptors that expose precall() (so policy/kill-switch behaviour is unchanged), re-raising GovernanceSignal to veto. LLMScope._enter runs it on a start record when the caller passes projected_tokens; a deny propagates out of __enter__ (the call body never runs) and the run record still flushes. Rides locked surfaces only (cost attr, metadata mechanism, PricingProvider SPI, Interceptor/GovernanceSignal); both capabilities default off. Full gate green (97.7%); changed files 96-100% covered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e example End-to-end validation against real backends surfaced two bugs, fixed here: - forgesight-otel: the OTLP/HTTP exporter does not append the /v1/traces signal path when endpoint is set explicitly, so `endpoint: http://host:4318` 404s. Append it automatically for http when the caller gives a base URL (an explicit path is kept). Now `http://localhost:4318` "just works"; gRPC still uses :4317 + protocol="grpc". Tested. - docker-compose.yml: jaeger tag `1.62` does not exist on Docker Hub -> `1.62.0`. - docs: README quick-start and playbooks 02/04 pointed OTLP/HTTP at the gRPC port :4317 with the default http protocol; corrected to :4318. examples/bedrock-e2e/: a real AWS Bedrock (Claude Haiku 4.5) call instrumented with ForgeSight, exporting traces to Jaeger (OTLP) + metrics to Prometheus, with the feat-023 audit trail and feat-026 attributed-cost/budget-utilization metrics, then verifying each landed. Validated live: 4-span GenAI-semconv trace in Jaeger with cost, attributed cost by team/owner, a tamper-evident audit chain, and Prometheus scrape. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ted) Stack: add a Grafana service to docker-compose with provisioned datasources (Prometheus + Jaeger) and a starter "ForgeSight — agent telemetry" dashboard (runs, cost, failures, p95 latency, cost-by-provider, token usage). UI at :3000, anonymous admin. examples/agents/: three self-contained, real AWS Bedrock agents instrumented with ForgeSight, exporting end-to-end to the local stack (traces → Jaeger, metrics → Prometheus/Grafana, audit trail, attributed-cost metrics): - react_agent — ReAct loop: run → step(iteration) → llm_call/tool_call - rag_agent — retrieve (vector-search tool) → generate (LLM) - multi_agent — supervisor delegating to nested researcher/writer sub-agent runs All three validated live against the stack: correct answers, hash-chained audit intact, and proper nested span trees in Jaeger (the multi-agent run exports as one 7-span tree across three agents). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Langfuse exporter posted to {host}/api/public/otel (the base), which 404s —
Langfuse's OTLP/HTTP ingest is the /v1/traces signal under it. Point at
{host}/api/public/otel/v1/traces via a tested `otlp_traces_endpoint` helper.
Verified live against Langfuse Cloud: a Bedrock run now lands as a trace with
cost + 2 observations (confirmed through the Langfuse public API).
Also: gitignore .env / .env.* so local secrets are never committed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Why: each agent example is a short-lived process — it serves Prometheus metrics on :9464 for only seconds and its counters reset per process, so Prometheus (pull, 5s scrape) rarely captures them and the Grafana dashboard under-counts (it showed "1 run" for several runs). Traces in Jaeger are always complete; only the pull metrics need a long-lived target. - Each agent now exposes run(client) (the body), reused by main() and by demo_all. - demo_all configures ForgeSight once, runs all agents under one runtime (metrics accumulate), and keeps :9464 up ~30s so Prometheus scrapes the real totals. Validated: while demo_all is alive, sum(agentforge_agent_runs_total)=5 (react + rag + supervisor/researcher/writer); it goes stale the instant the process exits — demonstrating the pull-vs-short-lived-job issue and the fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…xamples CI: add a `secret-scan` job (gitleaks over full git history, via its Docker image) so no credential can land before/after the repo goes public. Verified locally: 70 commits scanned, no leaks; .env was never committed (and is now gitignored). Docs (drift from feat-023/026 + the stack/examples work): - README: add forgesight-audit + live attributed-cost/pre-call-projection to the capability table, extras table, and packages list; point the local-stack callout at the full stack incl. Grafana; link the new examples/agents (Bedrock) examples. - New runbook: docs/runbooks/audit-trail.md (feat-023) + index row. - Playbook 03: add Grafana (datasources + dashboard), the OTLP/HTTP base-URL note, and the short-lived-process → Prometheus under-count gotcha with the demo_all fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sifiers Lead line (README hero + forgesight PyPI summary): "Instrument any AI agent in a few lines — then ship traces, cost, metrics, evals, budgets, and a tamper-evident audit trail to any backend by changing one line of config. OpenTelemetry-first. Vendor-neutral. Never an agent-code change." - forgesight (facade): punchy one-line description; keywords expanded (agent-observability, llm-observability, otel, genai, tracing, cost, finops, audit, governance); add Topic/Intended-Audience classifiers. - All 16 packages: add Intended Audience :: Information Technology + Topic :: System :: Monitoring + Topic :: Scientific/Engineering :: Artificial Intelligence for PyPI discoverability. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
examples/demo.py — a fully offline, self-contained demo (no backend/AWS/network): instruments a 'pr-reviewer' agent and prints the captured trace tree, derived cost, and a verified tamper-evident audit chain in ~1s. Recorded to docs/assets/demo.gif with VHS (docs/assets/demo.tape regenerates it). Embedded at the top of the README. The same telemetry ships to any backend by changing one config line — the demo just captures in-memory so it runs anywhere. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements feat-026 (spec on main). The live + pre-call delta over what already ships — both pure config over existing surfaces, no agent-code change.
Live attributed-cost metrics (
forgesight-core)forgesight.cost.attributed_usdcounter keyed by the stamped ownership dimensions (team/owner/…) — the live counterpart of feat-022's offline chargeback rollup. A record missing a dimension buckets to<unattributed>.forgesight.cost.budget_utilizationgauge (spend/cap). The instrument is a generic gauge in core; the budget semantics stay in governance, which records throughget_runtime().metrics— core never depends on governance (P1).AttributionMetricsConfigonMetricConfig; default off (P2).Pre-call budget projection (
forgesight-governance)BudgetInterceptorgains a projection mode +ProjectionConfig. A newprecall()estimates the call's cost from caller-declaredprojected_tokensvia the samePricingProviderand denies before the spend if a cap would breach — closing feat-020's post-hoc gap.output_token_estimate∈max_tokens|input_ratio|fixed;on_unpriced∈allow|deny.The minimal core hook
Runtime.run_precall_interceptorsruns only interceptors exposingprecall()— so policy/kill-switch behaviour is unchanged — re-raisingGovernanceSignalto veto.LLMScope._enterinvokes it on a start record when the caller passesprojected_tokens; a deny propagates out of__enter__(the call body never runs) and the run record still flushes (feat-020 invariant).Charter
Rides locked surfaces only (cost attr, metadata mechanism,
PricingProviderSPI,Interceptor/GovernanceSignal); custom metrics namespacedforgesight.cost.*(nevergen_ai.*, P4/ADR-0005); both capabilities default off.Validation (local, full gate)
ruff+mypy --strictclean, pytest 97.7%; all changed source files 96–100% covered.<unattributed>bucket + disabled; gauge setter; projection deny/allow + estimate modes +on_unpriced+ guard-not-commit +from_config; integration deny-before-call (body never runs) + projection-off behaves as feat-020 + livebudget_utilization.🤖 Generated with Claude Code