Skip to content

Add reporting, retry lifecycle, and dry-run functionality#231

Open
timkpaine wants to merge 1 commit into
mainfrom
tkp/rep
Open

Add reporting, retry lifecycle, and dry-run functionality#231
timkpaine wants to merge 1 commit into
mainfrom
tkp/rep

Conversation

@timkpaine

Copy link
Copy Markdown
Member

Introduce a structured reporting layer that captures evaluation metadata, timing, topology, retries, and failures without consuming result payloads, mirroring the existing evaluator/policy architecture.

  • ReportingPolicy shared core with span/run ContextVars for nested, thread/async-local isolation
  • ReportEvent model plus NoOp/InMemory/Logging/Composite/UI reporters and a bounded UI polling buffer
  • Tracing/metrics/alerts policies and OpenTelemetry tracing/metrics integration (exposed via otel/full/develop/test extras)
  • Structural reporting models and a Reporting{Evaluator,Model} taxonomy with placeholder vendor classes
  • Refactor LoggingEvaluator onto LoggingPolicy to share formatting and enable LoggingModel while preserving the existing import path and log output
  • Retry lifecycle events now carry run_id and child depth via current_span_depth(); reporter failures are isolated on reporting/retry paths
  • DryRunEvaluator with context-local planning guard; synthetic mode is non-transparent so results are not cached under real-run keys; node_key strips the dry-run evaluator layer while preserving non-evaluator options so it matches cache_key() for the logical node
  • ReportingStateStore preserves terminal outcomes while allowing retry streams to progress
  • Docs: reporting workflow, reporter options, OpenTelemetry install, reserved run/graph phases, extra payload keys, and dry-run synthetic-result warnings
  • Tests across utils/evaluators/models covering success/error flows, dry-run override recursion, cache composition, concurrent dry-run reuse, node-key semantics, retry event nesting, reporter failure isolation, and state folding

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Test Results

    1 files  ± 0      1 suites  ±0   3m 6s ⏱️ +11s
1 308 tests +83  1 306 ✅ +83  2 💤 ±0  0 ❌ ±0 
1 314 runs  +83  1 312 ✅ +83  2 💤 ±0  0 ❌ ±0 

Results for commit 94d0d1b. ± Comparison against base commit daa4c40.

♻️ This comment has been updated with latest results.

@codecov

codecov Bot commented Jun 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.99054% with 35 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.31%. Comparing base (daa4c40) to head (94d0d1b).

Files with missing lines Patch % Lines
ccflow/utils/reporting.py 93.57% 17 Missing and 8 partials ⚠️
ccflow/evaluators/reporting.py 95.12% 2 Missing and 2 partials ⚠️
ccflow/tests/evaluators/test_reporting.py 98.07% 4 Missing ⚠️
ccflow/tests/models/test_reporting.py 97.01% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #231      +/-   ##
==========================================
+ Coverage   93.09%   93.31%   +0.21%     
==========================================
  Files         169      175       +6     
  Lines       18748    19850    +1102     
  Branches     1235     1276      +41     
==========================================
+ Hits        17454    18523    +1069     
- Misses       1066     1089      +23     
- Partials      228      238      +10     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@timkpaine timkpaine marked this pull request as ready for review June 23, 2026 22:34
Introduce a structured reporting layer that captures evaluation metadata,
timing, topology, retries, and failures without consuming result payloads,
mirroring the existing evaluator/policy architecture.

- ReportingPolicy shared core with span/run ContextVars for nested,
  thread/async-local isolation
- ReportEvent model plus NoOp/InMemory/Logging/Composite/UI reporters and
  a bounded UI polling buffer
- Tracing/metrics/alerts policies and OpenTelemetry tracing/metrics
  integration (exposed via otel/full/develop/test extras)
- Structural reporting models and a <Vendor><Signal>Reporting{Evaluator,Model}
  taxonomy with placeholder vendor classes
- Refactor LoggingEvaluator onto LoggingPolicy to share formatting and
  enable LoggingModel while preserving the existing import path and log output
- Retry lifecycle events now carry run_id and child depth via
  current_span_depth(); reporter failures are isolated on reporting/retry paths
- DryRunEvaluator with context-local planning guard; synthetic mode is
  non-transparent so results are not cached under real-run keys; node_key
  strips the dry-run evaluator layer while preserving non-evaluator options
  so it matches cache_key() for the logical node
- ReportingStateStore preserves terminal outcomes while allowing retry streams
  to progress
- Docs: reporting workflow, reporter options, OpenTelemetry install, reserved
  run/graph phases, extra payload keys, and dry-run synthetic-result warnings
- Tests across utils/evaluators/models covering success/error flows, dry-run
  override recursion, cache composition, concurrent dry-run reuse, node-key
  semantics, retry event nesting, reporter failure isolation, and state folding

Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>

@ptomecek ptomecek left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took a pass through this. I really like how the reporting policy/evaluator/model split mirrors the retry trio (RetryPolicy/RetryEvaluator/RetryModel) - it slots in cleanly and reads exactly the way I'd expect. A few things I'd like to sort out before it goes in, left inline.

One non-code note: the description has a "Docs: reporting workflow, reporter options, ..." bullet, but I don't see any docs/ changes in the diff. Did those get dropped, or is the description just ahead of the code? Given how much public API this adds, I think we'll want the docs to land with it.

return self._run_with_reporting(context, **_descriptor(context))


class DryRunEvaluator(EvaluatorBase, ReportingPolicy):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DryRunEvaluator feels a bit out of place here. It's really a planning/preview tool rather than telemetry, but it's sitting in reporting.py and inheriting ReportingPolicy - I don't think anyone hunting for "dry run" would think to look under reporting, and inheriting the policy means it drags reporting config along even when you just want the plan.

It does emit QUEUED/SKIPPED so the dependency on reporting is real, but could we move it to common.py (or its own module) and compose reporting instead of inheriting it? At a minimum I'd like a note explaining why it lives here, since the placement isn't obvious.



# *****************************************************************************
# Vendor placeholders (not yet implemented)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we hold off on shipping these? Right now there are 9 classes that only raise NotImplementedError, and they're all in all - that's more dead names than working ones in this module's public surface, and we don't ship stub classes like this anywhere else in ccflow.

I'd rather add each one when its backend actually exists. If the point is just to stake out the taxonomy, a comment or a private list is a lot lighter than 9 exported classes we'd then have to keep around and eventually deprecate if the plan changes. Config can still reference an unimplemented path via PyObjectPath without a shipped class.

Comment thread ccflow/utils/reporting.py

# -- lifecycle hooks (override in subclasses) ----------------------------

def _on_start(self, ctx: ReportContext, span: "Optional[Span]") -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since LoggingEvaluator is the default evaluator, this hook runs on basically every model evaluation - and _event() builds a full ReportEvent here even when reporter is None, which is the common case. _emit then just discards it. I measured it at roughly 11us/eval across the three events (START/SUCCESS/END), all wasted when nobody's reporting.

Could we short-circuit when self.reporter is None before constructing the event? The inline logging in LoggingPolicy still needs to fire, but the base emit hooks shouldn't have to build anything if there's no sink.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants