19
19
from agents import Agent
20
20
from agents import AgentHooks
21
21
from agents import function_tool
22
+ from agents import handoff
22
23
from agents import ModelSettings
23
24
from agents import RunContextWrapper
24
25
from agents import Runner
25
26
from agents .extensions .handoff_prompt import prompt_with_handoff_instructions
27
+ from agents .extensions .handoff_prompt import RECOMMENDED_PROMPT_PREFIX
26
28
from agents .extensions .models .litellm_model import LitellmModel
29
+ from agents .handoffs import HandoffInputData
27
30
from agents .stream_events import RawResponsesStreamEvent
28
31
from agents .stream_events import RunItemStreamEvent
29
32
from braintrust import traced
58
61
@dataclass
59
62
class RunDependencies :
60
63
emitter : Emitter
64
+ llm : LLM
61
65
search_tool : SearchTool | None = None
62
66
63
67
@@ -66,6 +70,7 @@ class MyContext:
66
70
"""Context class to hold search tool and other dependencies"""
67
71
68
72
run_dependencies : RunDependencies | None = None
73
+ needs_compaction : bool = False
69
74
70
75
71
76
def short_tag (link : str , i : int ) -> str :
@@ -128,7 +133,7 @@ def llm_completion(
128
133
model = model_name ,
129
134
temperature = temperature ,
130
135
messages = messages ,
131
- tools = None ,
136
+ tools = [] ,
132
137
stream = stream ,
133
138
)
134
139
@@ -243,6 +248,7 @@ async def amain():
243
248
run_dependencies = RunDependencies (
244
249
search_tool = search_tool ,
245
250
emitter = emitter ,
251
+ llm = llm ,
246
252
)
247
253
)
248
254
# 1) start the streamed run (async)
@@ -291,17 +297,39 @@ def finalize_report():
291
297
}
292
298
293
299
294
- class VerboseHooks (AgentHooks [Any ]):
300
+ class CompactionHooks (AgentHooks [Any ]):
295
301
async def on_llm_start (
296
302
self ,
297
- context : RunContextWrapper [Any ],
303
+ context : RunContextWrapper [MyContext ],
298
304
agent : Agent [Any ],
299
305
system_prompt : Optional [str ],
300
- input_items : List [dict ], # alias: TResponseInputItem
306
+ input_items : List [dict ],
301
307
) -> None :
302
308
print (f"[{ agent .name } ] LLM start" )
303
309
print ("system_prompt:" , system_prompt )
304
310
print ("usage so far:" , context .usage .total_tokens )
311
+ usage = context .usage .total_tokens
312
+ if usage > 10000 :
313
+ context .context .needs_compaction = True
314
+
315
+
316
+ def compaction_input_filter (input_data : HandoffInputData ):
317
+ filtered_messages = []
318
+ for msg in input_data .input_history [:- 1 ]:
319
+ if isinstance (msg , dict ) and msg .get ("content" ) is not None :
320
+ # Convert tool messages to user messages to avoid API errors
321
+ if msg .get ("role" ) == "tool" :
322
+ filtered_msg = {
323
+ "role" : "user" ,
324
+ "content" : f"Tool response: { msg .get ('content' , '' )} " ,
325
+ }
326
+ filtered_messages .append (filtered_msg )
327
+ else :
328
+ filtered_messages .append (msg )
329
+
330
+ # Only proceed with compaction if we have valid messages
331
+ if filtered_messages :
332
+ return [filtered_messages [- 1 ]]
305
333
306
334
307
335
def construct_deep_research_agent (llm : LLM ) -> Agent :
@@ -313,7 +341,8 @@ def construct_deep_research_agent(llm: LLM) -> Agent:
313
341
api_key = llm .config .api_key ,
314
342
)
315
343
316
- DR_INSTRUCTIONS = """
344
+ DR_INSTRUCTIONS = f"""
345
+ { RECOMMENDED_PROMPT_PREFIX }
317
346
You are a deep-research agent. Work in explicit iterations:
318
347
1) PLAN: Decompose the user’s query into sub-questions and a step-by-step plan.
319
348
2) SEARCH: Use web_search to explore multiple angles, fanning out and searching in parallel.
@@ -343,7 +372,7 @@ def construct_deep_research_agent(llm: LLM) -> Agent:
343
372
# optional: let model choose tools freely
344
373
# tool_choice="auto", # if supported by your LitellmModel wrapper
345
374
),
346
- hooks = VerboseHooks (),
375
+ hooks = CompactionHooks (),
347
376
)
348
377
349
378
@@ -558,15 +587,35 @@ def dr_turn(
558
587
)
559
588
turn_event_stream_emitter .emit (kind = "done" , data = {"ok" : True })
560
589
return
561
-
562
- agent = construct_deep_research_agent (llm )
590
+ dr_agent = construct_deep_research_agent (llm )
591
+ compactor_agent = Agent (
592
+ name = "Compactor" ,
593
+ instructions = f"""
594
+ { RECOMMENDED_PROMPT_PREFIX }
595
+ Summarize the full conversation so far into JSON with keys:\n
596
+ - summary: concise timeline of what happened so far\n
597
+ - facts: bullet list of stable facts (IDs, URLs, constraints)\n
598
+ - open_questions: bullet list of TODOs / follow-ups\n
599
+ Set already_compacted=true to prevent immediate re-compaction.
600
+ Then hand off to deep research agent.
601
+ """ ,
602
+ output_type = dict ,
603
+ handoffs = [
604
+ handoff (
605
+ agent = dr_agent ,
606
+ input_filter = compaction_input_filter ,
607
+ )
608
+ ],
609
+ tool_use_behavior = "stop_on_first_tool" ,
610
+ )
563
611
ctx = MyContext (
564
612
run_dependencies = RunDependencies (
565
613
search_tool = search_tool ,
566
614
emitter = turn_event_stream_emitter ,
615
+ llm = llm ,
567
616
)
568
617
)
569
- bridge = StreamBridge (agent , messages , ctx , max_turns = 100 ).start ()
618
+ bridge = StreamBridge (compactor_agent , messages , ctx , max_turns = 100 ).start ()
570
619
for ev in bridge .events ():
571
620
if isinstance (ev , RunItemStreamEvent ):
572
621
pass
0 commit comments