A graph-based workflow node. _run_impl() IS the graph orchestration loop: - SETUP: build graph, seed triggers - LOOP: schedule ready nodes via NodeRunner, handle completions - FINALIZE: collect terminal outputs
| 144 | |
| 145 | |
| 146 | class Workflow(BaseNode): |
| 147 | """A graph-based workflow node. |
| 148 | |
| 149 | _run_impl() IS the graph orchestration loop: |
| 150 | - SETUP: build graph, seed triggers |
| 151 | - LOOP: schedule ready nodes via NodeRunner, handle completions |
| 152 | - FINALIZE: collect terminal outputs |
| 153 | """ |
| 154 | |
| 155 | rerun_on_resume: bool = Field(default=True) |
| 156 | |
| 157 | edges: list[EdgeItem] = Field( |
| 158 | description='Edges to build the workflow graph.', |
| 159 | default_factory=list, |
| 160 | ) |
| 161 | |
| 162 | max_concurrency: int | None = None |
| 163 | """Maximum parallel graph-scheduled nodes. None means unlimited. |
| 164 | |
| 165 | Only applies to nodes triggered by graph edges. Dynamic nodes |
| 166 | (via ctx.run_node()) are excluded — they are awaited inline by |
| 167 | their parent and throttling them would cause deadlock. |
| 168 | """ |
| 169 | |
| 170 | graph: Graph | None = Field( |
| 171 | description='The compiled workflow graph.', |
| 172 | default=None, |
| 173 | ) |
| 174 | |
| 175 | # --- Construction --- |
| 176 | |
| 177 | def model_post_init(self, context: Any) -> None: |
| 178 | super().model_post_init(context) |
| 179 | if self.edges and self.graph is None: |
| 180 | self.graph = self._build_graph() |
| 181 | self._validate_state_schema() |
| 182 | self._validate_no_task_mode_graph_nodes() |
| 183 | |
| 184 | def _build_graph(self) -> Graph: |
| 185 | """Convert edge definitions to a validated Graph.""" |
| 186 | graph = Graph.from_edge_items(self.edges) |
| 187 | graph.validate_graph() |
| 188 | return graph |
| 189 | |
| 190 | def _validate_no_task_mode_graph_nodes(self) -> None: |
| 191 | """Reject ``mode='task'`` LlmAgents that appear as static graph nodes. |
| 192 | |
| 193 | Task-mode agents are multi-turn — they pause for user replies and |
| 194 | expect the original ``node_input`` (the task brief) to remain visible |
| 195 | across re-dispatches. The workflow scheduler currently overwrites |
| 196 | ``node_input`` with the latest user message on every re-entry, so the |
| 197 | task brief is lost and the agent loses context. Until the scheduler |
| 198 | preserves the originating ``node_input`` on resume, task agents may |
| 199 | only be used: |
| 200 | |
| 201 | * as chat sub-agents of an LlmAgent coordinator (FC delegation |
| 202 | via ``_TaskAgentTool`` / ``_dispatch_task_fc``), or |
| 203 | * dispatched dynamically via ``ctx.run_node`` from a custom |
no outgoing calls