mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-22 07:15:10 +00:00
fix: dispatch execution_end hook on failed crew and flow executions (#6607)
* fix: dispatch execution_end hook on failed crew and flow executions
The `execution_end` interception point only fired after a successful
kickoff, so consumers never learned about failed runs. Crew kickoff
paths (`kickoff`/`akickoff`) and the flow runtime (`kickoff_async`,
`resume_async`) now dispatch it on the failure path too, with new
additive `status` ("completed"/"failed") and `error` fields on
`ExecutionEndContext`. Pairing flags guarantee exactly-once dispatch,
keep the start/end pairing invariant, and the original exception
propagates unchanged.
* fix: track execution_end pairing per invocation for reentrant flows
Reentrant kickoffs on the same Flow instance are supported (usage
aggregation already accommodates them), but the instance-level pairing
booleans let an inner kickoff's completion mark the outer execution as
ended, skipping the outer failure's `execution_end`. The pairing state
now lives in each `kickoff_async` invocation's locals, and the resume
path passes a per-invocation holder into `_resume_async_body`. Crew
keeps its instance flags since crew kickoffs are not reentrant on the
same instance (`kickoff_for_each` copies the crew).
This commit is contained in:
@@ -18,7 +18,7 @@ Four interception points cover the boundaries:
|
||||
| `EXECUTION_START` | A crew or flow is about to begin | inputs `dict` |
|
||||
| `INPUT` | Resolved inputs for the execution | inputs `dict` |
|
||||
| `OUTPUT` | The final result is ready | the output object |
|
||||
| `EXECUTION_END` | The execution has finished | the output object |
|
||||
| `EXECUTION_END` | The execution has finished (success or failure) | the output object, or `None` on failure |
|
||||
|
||||
For a crew, the output payload is a `CrewOutput`. For a flow, it is the final
|
||||
flow-method result.
|
||||
@@ -68,7 +68,9 @@ class OutputContext(InterceptionContext):
|
||||
output: Any # The output object
|
||||
|
||||
class ExecutionEndContext(InterceptionContext):
|
||||
output: Any # The output object
|
||||
output: Any # The output object (None when status == "failed")
|
||||
status: str # "completed" or "failed"
|
||||
error: BaseException | None # The exception when status == "failed"
|
||||
```
|
||||
|
||||
<Note>
|
||||
@@ -136,6 +138,28 @@ def redact_emails(ctx):
|
||||
payload from earlier hooks; the final rewritten value is what `kickoff()`
|
||||
returns.
|
||||
|
||||
### Observing Failures
|
||||
|
||||
`EXECUTION_END` fires exactly once per execution, on success and on failure
|
||||
alike. When the run raises — a task error, a flow-method exception, or a
|
||||
`HookAborted` from an earlier point — the hook receives `status="failed"` with
|
||||
the exception in `ctx.error`, and the original exception still propagates out
|
||||
of `kickoff()` unchanged:
|
||||
|
||||
```python
|
||||
@on(InterceptionPoint.EXECUTION_END)
|
||||
def report_outcome(ctx):
|
||||
if ctx.status == "failed":
|
||||
notify_policy_engine(status="failed", error=repr(ctx.error))
|
||||
else:
|
||||
notify_policy_engine(status="completed")
|
||||
```
|
||||
|
||||
Two caveats: `EXECUTION_END` does not fire when `EXECUTION_START` never
|
||||
dispatched (an abort at start counts as the execution never beginning), and
|
||||
raising `HookAborted` from a failure-path `EXECUTION_END` dispatch is ignored —
|
||||
there is nothing left to abort, and the original error wins.
|
||||
|
||||
## Ordering
|
||||
|
||||
For a crew run the boundary order is:
|
||||
|
||||
Reference in New Issue
Block a user