@@ -152,6 +152,51 @@ def _build_timeout_result(
152152 model = model ,
153153 )
154154
155+ def _build_budget_skip_result (
156+ self ,
157+ stats : AgentRunStats ,
158+ all_tool_calls : List [Dict [str , Any ]],
159+ models_used : List [str ],
160+ elapsed_s : float ,
161+ timeout_s : int ,
162+ stage_name : str ,
163+ remaining_budget : float ,
164+ min_stage_budget_s : int ,
165+ ctx : Optional [AgentContext ] = None ,
166+ parse_dashboard : bool = True ,
167+ ) -> OrchestratorResult :
168+ """Build a result for budget-insufficient stage skip (non-timeout semantics)."""
169+ stats .total_duration_s = round (elapsed_s , 2 )
170+ stats .models_used = list (dict .fromkeys (models_used ))
171+ dashboard = None
172+ content = ""
173+ if ctx is not None :
174+ dashboard , content = self ._resolve_final_output (ctx , parse_dashboard = parse_dashboard )
175+ if parse_dashboard and dashboard is not None :
176+ dashboard = self ._mark_partial_dashboard (
177+ dashboard ,
178+ note = "多 Agent 预算不足,以下结论基于已完成阶段自动降级生成。" ,
179+ )
180+ ctx .set_data ("final_dashboard" , dashboard )
181+ content = json .dumps (dashboard , ensure_ascii = False , indent = 2 )
182+
183+ return OrchestratorResult (
184+ success = bool (content ) if (not parse_dashboard or dashboard is not None ) else False ,
185+ content = content ,
186+ dashboard = dashboard ,
187+ error = (
188+ f"Pipeline skipped before stage '{ stage_name } ' due to insufficient budget "
189+ f"({ remaining_budget :.1f} s remaining, minimum { min_stage_budget_s } s required)"
190+ ),
191+ stats = stats ,
192+ total_steps = stats .total_stages ,
193+ total_tokens = stats .total_tokens ,
194+ tool_calls_log = all_tool_calls ,
195+ provider = stats .models_used [0 ] if stats .models_used else "" ,
196+ model = ", " .join (stats .models_used ),
197+ )
198+
199+
155200 def _prepare_agent (self , agent : Any ) -> Any :
156201 """Apply orchestrator-level runtime settings to a child agent."""
157202 if hasattr (agent , "max_steps" ):
@@ -313,22 +358,31 @@ def _execute_pipeline(
313358 # completed so that the first stage always gets a chance to run
314359 # even when the total budget is small.
315360 _MIN_STAGE_BUDGET_S = 15
361+ _MIN_TOOLLESS_STAGE_BUDGET_S = 0
316362
317363 while index < len (agents ):
318364 agent = agents [index ]
319365 elapsed_s = time .time () - t0
320366 remaining_budget = timeout_s - elapsed_s if timeout_s else None
321- budget_exhausted = (
367+ tool_names = getattr (agent , "tool_names" , None )
368+ stage_min_budget_s = (
369+ _MIN_TOOLLESS_STAGE_BUDGET_S
370+ if isinstance (tool_names , (list , tuple , set , frozenset )) and len (tool_names ) == 0
371+ else _MIN_STAGE_BUDGET_S
372+ )
373+ timeout_exhausted = (
322374 timeout_s
323375 and remaining_budget is not None
324- and (
325- remaining_budget <= 0
326- or (index > 0 and remaining_budget < _MIN_STAGE_BUDGET_S )
327- )
376+ and remaining_budget <= 0
328377 )
329- if budget_exhausted :
330- reason = "timed out" if remaining_budget <= 0 else f"insufficient budget ({ remaining_budget :.1f} s < { _MIN_STAGE_BUDGET_S } s)"
331- logger .error ("[Orchestrator] pipeline %s before stage '%s'" , reason , agent .agent_name )
378+ budget_guard_triggered = (
379+ timeout_s
380+ and remaining_budget is not None
381+ and index > 0
382+ and remaining_budget < stage_min_budget_s
383+ )
384+ if timeout_exhausted :
385+ logger .error ("[Orchestrator] pipeline timed out before stage '%s'" , agent .agent_name )
332386 if progress_callback :
333387 progress_callback ({
334388 "type" : "pipeline_timeout" ,
@@ -346,6 +400,33 @@ def _execute_pipeline(
346400 parse_dashboard = parse_dashboard ,
347401 )
348402
403+ if budget_guard_triggered :
404+ logger .warning (
405+ "[Orchestrator] pipeline insufficient budget before stage '%s' (%.1fs remaining, min %ds)" ,
406+ agent .agent_name ,
407+ remaining_budget ,
408+ stage_min_budget_s ,
409+ )
410+ if progress_callback :
411+ progress_callback ({
412+ "type" : "pipeline_timeout" ,
413+ "stage" : agent .agent_name ,
414+ "elapsed" : round (elapsed_s , 2 ),
415+ "timeout" : timeout_s ,
416+ })
417+ return self ._build_budget_skip_result (
418+ stats ,
419+ all_tool_calls ,
420+ models_used ,
421+ elapsed_s ,
422+ timeout_s ,
423+ agent .agent_name ,
424+ remaining_budget ,
425+ stage_min_budget_s ,
426+ ctx = ctx ,
427+ parse_dashboard = parse_dashboard ,
428+ )
429+
349430 if (
350431 self .mode == "specialist"
351432 and agent .agent_name == "decision"
0 commit comments