feat(function_pod): add post-run hook mechanism (ITL-523)#226
feat(function_pod): add post-run hook mechanism (ITL-523)#226kurodo3[bot] wants to merge 10 commits into
Conversation
…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 Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
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.pydefiningInvocationStatus,RunStats,PodContext,PostRunPayload,HookConfig, andPostRunHook. - Implemented hook registration and firing in
_FunctionPodBasevia_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 fromorcapod.__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.
| if exc is not None: | ||
| raise exc | ||
| return out_tag, output_data |
There was a problem hiding this comment.
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.
| if exc is not None: | ||
| raise exc | ||
| return out_tag, output_data |
There was a problem hiding this comment.
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.
| if exc is not None: | ||
| raise exc | ||
| return out_tag, output_data |
There was a problem hiding this comment.
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.
| if exc is not None: | ||
| raise exc | ||
| return out_tag, output_data |
There was a problem hiding this comment.
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.
| 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. |
There was a problem hiding this comment.
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>
Review round 1 — changes madeAll 9 Copilot comments addressed in commit Traceback preservation (comments 1, 4, 5, 7)The original code stored exceptions outside the 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_dataBare Empty-hooks fast-path (comments 2, 3, 6, 9)All four methods now begin with an early return:
Pods without hooks now pay zero overhead — no timing, no try/except, identical exception behaviour to calling Extracted
|
| 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 |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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>
Review round 2 — changes madeBoth comments addressed in commit
|
Summary
FunctionPodthat fires after every invocation (computed, cache hit, or error) before the result is emitted downstreamsrc/orcapod/hooks.pywith all public types:InvocationStatus(COMPUTED/HIT/ERROR),RunStats,PodContext,PostRunPayload,HookConfig,PostRunHook_FunctionPodBasegainsadd_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) updatedCachedFunctionPodoverrides_invoke_with_hooksto detectInvocationStatus.HITviaRESULT_COMPUTED_FLAGmeta@function_poddecorator acceptspost_run_hooks=for convenience registrationorcapod.__init__tests/test_core/function_pod/test_post_run_hooks.py; 4426 tests passing totalTest plan
on_error="log") logs and continues; result still returnedCachedFunctionPodfires withInvocationStatus.HITInvocationStatus.ERROR, original exception re-raised after hooksoutput=None,record_id_hash=None,status=COMPUTEDasync_executeorchestrator path@function_pod(post_run_hooks=[fn])registers hooks identically toadd_post_run_hook_post_run_hooksis empty by default, no payload constructed (zero overhead)orcapodFixes ITL-523
🤖 Generated with Claude Code