Skip to content

feat(function_pod): add post-run hook mechanism (ITL-523)#226

Open
kurodo3[bot] wants to merge 10 commits into
mainfrom
eywalker/itl-523-function-pods-configurable-post-run-hook-invoked-after-every
Open

feat(function_pod): add post-run hook mechanism (ITL-523)#226
kurodo3[bot] wants to merge 10 commits into
mainfrom
eywalker/itl-523-function-pods-configurable-post-run-hook-invoked-after-every

Conversation

@kurodo3

@kurodo3 kurodo3 Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds a per-pod post-run hook mechanism to FunctionPod that fires after every invocation (computed, cache hit, or error) before the result is emitted downstream
  • Introduces src/orcapod/hooks.py with all public types: InvocationStatus (COMPUTED/HIT/ERROR), RunStats, PodContext, PostRunPayload, HookConfig, PostRunHook
  • _FunctionPodBase gains add_post_run_hook(), _invoke_with_hooks(), _async_invoke_with_hooks(), and _fire_post_run_hooks(); all three call sites (sequential iteration, concurrent iteration, async orchestrator pipeline) updated
  • CachedFunctionPod overrides _invoke_with_hooks to detect InvocationStatus.HIT via RESULT_COMPUTED_FLAG meta
  • @function_pod decorator accepts post_run_hooks= for convenience registration
  • All 7 hook types re-exported from orcapod.__init__
  • 20 new tests in tests/test_core/function_pod/test_post_run_hooks.py; 4426 tests passing total

Test plan

  • Single hook fires with correct payload (record_id_hash, tag, input, output, stats, pod context)
  • Multiple hooks fire in registration order
  • Fail-loud hook error propagates exception and stops remaining hooks
  • Resilient hook (on_error="log") logs and continues; result still returned
  • Cache hit status — second call to CachedFunctionPod fires with InvocationStatus.HIT
  • Error status — pod function raises; hook fires with InvocationStatus.ERROR, original exception re-raised after hooks
  • Filtered output — output=None, record_id_hash=None, status=COMPUTED
  • Parallel execution — hooks fire for all inputs through concurrent path
  • Async execute — hooks fire through async_execute orchestrator path
  • Decorator convenience — @function_pod(post_run_hooks=[fn]) registers hooks identically to add_post_run_hook
  • Empty hooks — _post_run_hooks is empty by default, no payload constructed (zero overhead)
  • Public API — all types importable directly from orcapod
  • Full test suite regression run: 4426 passed

Fixes ITL-523

🤖 Generated with Claude Code

kurodo3 Bot and others added 8 commits July 14, 2026 01:14
…all site (ITL-523)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…hooks (ITL-523)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…oks (ITL-523)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…status (ITL-523)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…public API (ITL-523)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.39535% with 24 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/orcapod/core/cached_function_pod.py 47.36% 20 Missing ⚠️
src/orcapod/core/function_pod.py 92.45% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a per-FunctionPod post-run hook mechanism that fires after each invocation (computed, cache hit, or error) and provides hook implementations with a structured payload and run stats. It adds the public hook type surface (orcapod.hooks + orcapod re-exports) and wires hook firing into all execution paths (sequential, concurrent, and async_execute), with CachedFunctionPod reporting cache-hit status.

Changes:

  • Added src/orcapod/hooks.py defining InvocationStatus, RunStats, PodContext, PostRunPayload, HookConfig, and PostRunHook.
  • Implemented hook registration and firing in _FunctionPodBase via _invoke_with_hooks() / _async_invoke_with_hooks() and updated all call sites to use them.
  • Added @function_pod(..., post_run_hooks=...) convenience registration, re-exported hook types from orcapod.__init__, and added a comprehensive test suite.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
tests/test_core/function_pod/test_post_run_hooks.py Adds coverage for hook payload correctness, ordering, failure semantics, cache-hit status, concurrent + async paths, and decorator registration.
superpowers/specs/2026-07-14-itl-523-post-run-hook-design.md Design spec documenting the hook API and semantics.
superpowers/plans/2026-07-14-itl-523-post-run-hook.md Implementation plan for the hook feature.
src/orcapod/hooks.py Introduces public hook and payload types.
src/orcapod/core/function_pod.py Adds hook storage/registration + invoke wrappers; updates sequential/concurrent/async execution call sites; adds decorator param.
src/orcapod/core/cached_function_pod.py Overrides invoke wrappers to set InvocationStatus.HIT based on RESULT_COMPUTED_FLAG.
src/orcapod/init.py Re-exports hook types as part of the public API.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/orcapod/core/function_pod.py
Comment thread src/orcapod/core/function_pod.py Outdated
Comment on lines +294 to +296
if exc is not None:
raise exc
return out_tag, output_data

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Restructured the error path to fire hooks inside the except block and then use a bare raise. The original _invoke_with_hooks stored the exception outside the except block and re-raised it with raise exc, which added a wrapper frame to the traceback. Now the except branch calls _fire_post_run_hooks and then issues a bare raise, so the original traceback is preserved exactly as if _invoke_with_hooks were not in the call stack.

Comment thread src/orcapod/core/function_pod.py
Comment thread src/orcapod/core/function_pod.py Outdated
Comment on lines +354 to +356
if exc is not None:
raise exc
return out_tag, output_data

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Same restructuring as the sync version: error hooks fire inside the except block followed by bare raise, which preserves the original traceback from async_process_data without adding a wrapper frame.

Comment thread src/orcapod/core/cached_function_pod.py
Comment thread src/orcapod/core/cached_function_pod.py Outdated
Comment on lines +221 to +223
if exc is not None:
raise exc
return out_tag, output_data

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Same restructuring as the base class: the error path now fires hooks inside the except block and uses bare raise to re-raise with the original traceback. The stored-exception-then-raise exc pattern is gone from both sync and async variants in CachedFunctionPod.

Comment thread src/orcapod/core/cached_function_pod.py
Comment thread src/orcapod/core/cached_function_pod.py Outdated
Comment on lines +285 to +287
if exc is not None:
raise exc
return out_tag, output_data

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Restructured to fire error hooks inside the except block and use bare raise, matching the base class pattern. The original traceback from async_process_data is now preserved exactly.

Comment thread src/orcapod/hooks.py Outdated
Comment on lines +41 to +45
duration_ms: Wall-clock time in milliseconds.
status: Whether the result was freshly computed, a cache hit, or an error.
started_at: UTC timestamp when the invocation started.
finished_at: UTC timestamp when the invocation finished (after hooks fire).
error: The exception raised, if ``status == ERROR``; ``None`` otherwise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Updated the RunStats docstring:\n- finished_at: was "after hooks fire" — now correctly says "when the compute-or-lookup step completed, before hooks fire"\n- duration_ms: clarified it measures compute-or-lookup time only, hook execution time is excluded\n\nThe implementation already captured finished_at before firing hooks (it had to, since the payload is passed into the hooks); the docstring just didn't match.

…ast-path, docstring (ITL-523)

- Add fast-path early return to all four _invoke_with_hooks / _async_invoke_with_hooks
  methods (base + CachedFunctionPod) so pods without hooks pay zero overhead and retain
  exactly the same exception behaviour as before
- Restructure error path to fire hooks inside the except block and use bare raise, so
  the original traceback is preserved exactly instead of adding a wrapper frame
- Extract _build_post_run_payload() helper on _FunctionPodBase to eliminate the
  duplicated payload-construction code across sync/async/cached variants
- Fix RunStats docstring: finished_at and duration_ms now correctly describe
  compute-or-lookup completion time, before hooks fire

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@kurodo3

kurodo3 Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Review round 1 — changes made

All 9 Copilot comments addressed in commit 78bcaf67.

Traceback preservation (comments 1, 4, 5, 7)

The original code stored exceptions outside the except block and re-raised them with raise exc, which added a wrapper frame to the traceback. All four _invoke_with_hooks / _async_invoke_with_hooks methods (base class + CachedFunctionPod, sync + async) have been restructured to the try-except-else pattern:

try:
    out_tag, output_data = self.process_data(tag, data, logger=logger)
except Exception as exc:
    finished_at = datetime.now(timezone.utc)
    self._fire_post_run_hooks(self._build_post_run_payload(..., InvocationStatus.ERROR, exc))
    raise  # bare raise — preserves original traceback exactly

# success path
finished_at = datetime.now(timezone.utc)
self._fire_post_run_hooks(self._build_post_run_payload(..., InvocationStatus.COMPUTED, None))
return out_tag, output_data

Bare raise inside the except block re-raises without adding a wrapper frame.

Empty-hooks fast-path (comments 2, 3, 6, 9)

All four methods now begin with an early return:

  • _FunctionPodBase._invoke_with_hooks: if not self._post_run_hooks: return self.process_data(...)
  • _FunctionPodBase._async_invoke_with_hooks: if not self._post_run_hooks: return await self.async_process_data(...)
  • CachedFunctionPod._invoke_with_hooks: same
  • CachedFunctionPod._async_invoke_with_hooks: same

Pods without hooks now pay zero overhead — no timing, no try/except, identical exception behaviour to calling process_data directly.

Extracted _build_post_run_payload helper

As a side effect of the restructuring (the success and error paths now build payloads independently), a _build_post_run_payload() helper was extracted on _FunctionPodBase to avoid duplicating payload construction across all four methods. CachedFunctionPod inherits it.

RunStats docstring fix (comment 8)

  • finished_at: was "after hooks fire" — corrected to "when the compute-or-lookup step completed, before hooks fire"
  • duration_ms: clarified it measures compute-or-lookup time only; hook execution time is excluded

Test results

All 20 hook tests pass, full suite 4426 passed with no regressions.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Comment on lines +187 to +195
if output_data is not None:
meta = output_data.as_dict(columns=ColumnConfig(meta=True))
status = (
InvocationStatus.HIT
if meta.get(self.RESULT_COMPUTED_FLAG) is False
else InvocationStatus.COMPUTED
)
else:
status = InvocationStatus.COMPUTED

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Replaced output_data.as_dict(columns=ColumnConfig(meta=True)) with output_data.get_meta_value(self.RESULT_COMPUTED_FLAG). get_meta_value reads directly from the internal _meta dict in O(1) without materializing any output data. Also removed the now-unused ColumnConfig import from the file.

Comment on lines +245 to +253
if output_data is not None:
meta = output_data.as_dict(columns=ColumnConfig(meta=True))
status = (
InvocationStatus.HIT
if meta.get(self.RESULT_COMPUTED_FLAG) is False
else InvocationStatus.COMPUTED
)
else:
status = InvocationStatus.COMPUTED

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Same as the sync path — replaced as_dict(columns=ColumnConfig(meta=True)) with output_data.get_meta_value(self.RESULT_COMPUTED_FLAG) in _async_invoke_with_hooks as well.

…_FLAG (ITL-523)

Replace output_data.as_dict(columns=ColumnConfig(meta=True)) with
output_data.get_meta_value(RESULT_COMPUTED_FLAG) in both sync and async
_invoke_with_hooks overrides. The old approach materialized the full output
datagram dict (data + meta) just to read a single boolean flag; get_meta_value
reads directly from the internal _meta dict in O(1) without any allocation.
Also removes the now-unused ColumnConfig import from the file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@kurodo3

kurodo3 Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Review round 2 — changes made

Both comments addressed in commit 6b5f1a29.

as_dict()get_meta_value() in both hook overrides (both comments)

In CachedFunctionPod._invoke_with_hooks and _async_invoke_with_hooks, the cache-hit detection was:

meta = output_data.as_dict(columns=ColumnConfig(meta=True))
if meta.get(self.RESULT_COMPUTED_FLAG) is False:

as_dict(columns=ColumnConfig(meta=True)) materializes the full output datagram (data payload + meta) into a Python dict just to read a single boolean flag. Replaced with:

if output_data.get_meta_value(self.RESULT_COMPUTED_FLAG) is False:

get_meta_value reads directly from the internal _meta dict in O(1) — no datagram materialization, no allocation. The ColumnConfig import is now unused and has been removed.

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.

1 participant