Create a child controller that aborts when its parent does. Aborting the child does NOT propagate up to the parent — that's the one-way semantic the streaming executor relies on (sibling abort cancels in-flight tools without ending the turn). Cleanup: the parent listener is registe
(parent: AbortController)
| 86 | |
| 87 | |
| 88 | def create_child_abort_controller(parent: AbortController) -> AbortController: |
| 89 | """Create a child controller that aborts when its parent does. |
| 90 | |
| 91 | Aborting the child does NOT propagate up to the parent — that's the |
| 92 | one-way semantic the streaming executor relies on (sibling abort |
| 93 | cancels in-flight tools without ending the turn). |
| 94 | |
| 95 | Cleanup: the parent listener is registered ``once=True`` so a single |
| 96 | fire detaches it. Additionally, when the child aborts on its own |
| 97 | (e.g. permission rejection), we proactively remove the parent |
| 98 | listener — otherwise long-lived parents accumulate one dead listener |
| 99 | per child, and the streaming executor creates one child per tool. |
| 100 | """ |
| 101 | child = AbortController() |
| 102 | |
| 103 | if parent.signal.aborted: |
| 104 | child.abort(parent.signal.reason) |
| 105 | return child |
| 106 | |
| 107 | def _on_parent_abort() -> None: |
| 108 | child.abort(parent.signal.reason) |
| 109 | |
| 110 | registered = parent.signal.add_listener(_on_parent_abort, once=True) |
| 111 | |
| 112 | def _detach_parent_listener() -> None: |
| 113 | parent.signal.remove_listener(registered) |
| 114 | |
| 115 | child.signal.add_listener(_detach_parent_listener, once=True) |
| 116 | return child |