@@ -52,6 +52,7 @@ def __init__(self):
5252 self .tasks = {} # task_id -> {status, result, command}
5353 self ._notification_queue = [] # completed task results
5454 self ._lock = threading .Lock ()
55+ self ._condition = threading .Condition (self ._lock )
5556
5657 def run (self , command : str ) -> str :
5758 """Start a background thread, return task_id immediately."""
@@ -67,8 +68,12 @@ def _execute(self, task_id: str, command: str):
6768 """Thread target: run subprocess, capture output, push to queue."""
6869 try :
6970 r = subprocess .run (
70- command , shell = True , cwd = WORKDIR ,
71- capture_output = True , text = True , timeout = 300
71+ command ,
72+ shell = True ,
73+ cwd = WORKDIR ,
74+ capture_output = True ,
75+ text = True ,
76+ timeout = 300 ,
7277 )
7378 output = (r .stdout + r .stderr ).strip ()[:50000 ]
7479 status = "completed"
@@ -80,29 +85,49 @@ def _execute(self, task_id: str, command: str):
8085 status = "error"
8186 self .tasks [task_id ]["status" ] = status
8287 self .tasks [task_id ]["result" ] = output or "(no output)"
83- with self ._lock :
84- self ._notification_queue .append ({
85- "task_id" : task_id ,
86- "status" : status ,
87- "command" : command [:80 ],
88- "result" : (output or "(no output)" )[:500 ],
89- })
88+ with self ._condition :
89+ self ._notification_queue .append (
90+ {
91+ "task_id" : task_id ,
92+ "status" : status ,
93+ "command" : command [:80 ],
94+ "result" : (output or "(no output)" )[:500 ],
95+ }
96+ )
97+ self ._condition .notify_all ()
9098
9199 def check (self , task_id : str = None ) -> str :
92100 """Check status of one task or list all."""
93101 if task_id :
94102 t = self .tasks .get (task_id )
95103 if not t :
96104 return f"Error: Unknown task { task_id } "
97- return f"[{ t ['status' ]} ] { t ['command' ][:60 ]} \n { t .get ('result' ) or '(running)' } "
105+ return (
106+ f"[{ t ['status' ]} ] { t ['command' ][:60 ]} \n { t .get ('result' ) or '(running)' } "
107+ )
98108 lines = []
99109 for tid , t in self .tasks .items ():
100110 lines .append (f"{ tid } : [{ t ['status' ]} ] { t ['command' ][:60 ]} " )
101111 return "\n " .join (lines ) if lines else "No background tasks."
102112
103113 def drain_notifications (self ) -> list :
104114 """Return and clear all pending completion notifications."""
105- with self ._lock :
115+ with self ._condition :
116+ notifs = list (self ._notification_queue )
117+ self ._notification_queue .clear ()
118+ return notifs
119+
120+ def _has_running_tasks_locked (self ) -> bool :
121+ return any (task ["status" ] == "running" for task in self .tasks .values ())
122+
123+ def has_running_tasks (self ) -> bool :
124+ with self ._condition :
125+ return self ._has_running_tasks_locked ()
126+
127+ def wait_for_notifications (self ) -> list :
128+ with self ._condition :
129+ while not self ._notification_queue and self ._has_running_tasks_locked ():
130+ self ._condition .wait ()
106131 notifs = list (self ._notification_queue )
107132 self ._notification_queue .clear ()
108133 return notifs
@@ -111,25 +136,48 @@ def drain_notifications(self) -> list:
111136BG = BackgroundManager ()
112137
113138
139+ def inject_background_results (messages : list , notifs : list ) -> bool :
140+ if notifs and messages :
141+ notif_text = "\n " .join (
142+ f"[bg:{ n ['task_id' ]} ] { n ['status' ]} : { n ['result' ]} " for n in notifs
143+ )
144+ messages .append (
145+ {
146+ "role" : "user" ,
147+ "content" : f"<background-results>\n { notif_text } \n </background-results>" ,
148+ }
149+ )
150+ return True
151+ return False
152+
153+
114154# -- Tool implementations --
115155def safe_path (p : str ) -> Path :
116156 path = (WORKDIR / p ).resolve ()
117157 if not path .is_relative_to (WORKDIR ):
118158 raise ValueError (f"Path escapes workspace: { p } " )
119159 return path
120160
161+
121162def run_bash (command : str ) -> str :
122163 dangerous = ["rm -rf /" , "sudo" , "shutdown" , "reboot" , "> /dev/" ]
123164 if any (d in command for d in dangerous ):
124165 return "Error: Dangerous command blocked"
125166 try :
126- r = subprocess .run (command , shell = True , cwd = WORKDIR ,
127- capture_output = True , text = True , timeout = 120 )
167+ r = subprocess .run (
168+ command ,
169+ shell = True ,
170+ cwd = WORKDIR ,
171+ capture_output = True ,
172+ text = True ,
173+ timeout = 120 ,
174+ )
128175 out = (r .stdout + r .stderr ).strip ()
129176 return out [:50000 ] if out else "(no output)"
130177 except subprocess .TimeoutExpired :
131178 return "Error: Timeout (120s)"
132179
180+
133181def run_read (path : str , limit : int = None ) -> str :
134182 try :
135183 lines = safe_path (path ).read_text ().splitlines ()
@@ -139,6 +187,7 @@ def run_read(path: str, limit: int = None) -> str:
139187 except Exception as e :
140188 return f"Error: { e } "
141189
190+
142191def run_write (path : str , content : str ) -> str :
143192 try :
144193 fp = safe_path (path )
@@ -148,6 +197,7 @@ def run_write(path: str, content: str) -> str:
148197 except Exception as e :
149198 return f"Error: { e } "
150199
200+
151201def run_edit (path : str , old_text : str , new_text : str ) -> str :
152202 try :
153203 fp = safe_path (path )
@@ -161,57 +211,113 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
161211
162212
163213TOOL_HANDLERS = {
164- "bash" : lambda ** kw : run_bash (kw ["command" ]),
165- "read_file" : lambda ** kw : run_read (kw ["path" ], kw .get ("limit" )),
166- "write_file" : lambda ** kw : run_write (kw ["path" ], kw ["content" ]),
167- "edit_file" : lambda ** kw : run_edit (kw ["path" ], kw ["old_text" ], kw ["new_text" ]),
168- "background_run" : lambda ** kw : BG .run (kw ["command" ]),
214+ "bash" : lambda ** kw : run_bash (kw ["command" ]),
215+ "read_file" : lambda ** kw : run_read (kw ["path" ], kw .get ("limit" )),
216+ "write_file" : lambda ** kw : run_write (kw ["path" ], kw ["content" ]),
217+ "edit_file" : lambda ** kw : run_edit (kw ["path" ], kw ["old_text" ], kw ["new_text" ]),
218+ "background_run" : lambda ** kw : BG .run (kw ["command" ]),
169219 "check_background" : lambda ** kw : BG .check (kw .get ("task_id" )),
170220}
171221
172222TOOLS = [
173- {"name" : "bash" , "description" : "Run a shell command (blocking)." ,
174- "input_schema" : {"type" : "object" , "properties" : {"command" : {"type" : "string" }}, "required" : ["command" ]}},
175- {"name" : "read_file" , "description" : "Read file contents." ,
176- "input_schema" : {"type" : "object" , "properties" : {"path" : {"type" : "string" }, "limit" : {"type" : "integer" }}, "required" : ["path" ]}},
177- {"name" : "write_file" , "description" : "Write content to file." ,
178- "input_schema" : {"type" : "object" , "properties" : {"path" : {"type" : "string" }, "content" : {"type" : "string" }}, "required" : ["path" , "content" ]}},
179- {"name" : "edit_file" , "description" : "Replace exact text in file." ,
180- "input_schema" : {"type" : "object" , "properties" : {"path" : {"type" : "string" }, "old_text" : {"type" : "string" }, "new_text" : {"type" : "string" }}, "required" : ["path" , "old_text" , "new_text" ]}},
181- {"name" : "background_run" , "description" : "Run command in background thread. Returns task_id immediately." ,
182- "input_schema" : {"type" : "object" , "properties" : {"command" : {"type" : "string" }}, "required" : ["command" ]}},
183- {"name" : "check_background" , "description" : "Check background task status. Omit task_id to list all." ,
184- "input_schema" : {"type" : "object" , "properties" : {"task_id" : {"type" : "string" }}}},
223+ {
224+ "name" : "bash" ,
225+ "description" : "Run a shell command (blocking)." ,
226+ "input_schema" : {
227+ "type" : "object" ,
228+ "properties" : {"command" : {"type" : "string" }},
229+ "required" : ["command" ],
230+ },
231+ },
232+ {
233+ "name" : "read_file" ,
234+ "description" : "Read file contents." ,
235+ "input_schema" : {
236+ "type" : "object" ,
237+ "properties" : {"path" : {"type" : "string" }, "limit" : {"type" : "integer" }},
238+ "required" : ["path" ],
239+ },
240+ },
241+ {
242+ "name" : "write_file" ,
243+ "description" : "Write content to file." ,
244+ "input_schema" : {
245+ "type" : "object" ,
246+ "properties" : {"path" : {"type" : "string" }, "content" : {"type" : "string" }},
247+ "required" : ["path" , "content" ],
248+ },
249+ },
250+ {
251+ "name" : "edit_file" ,
252+ "description" : "Replace exact text in file." ,
253+ "input_schema" : {
254+ "type" : "object" ,
255+ "properties" : {
256+ "path" : {"type" : "string" },
257+ "old_text" : {"type" : "string" },
258+ "new_text" : {"type" : "string" },
259+ },
260+ "required" : ["path" , "old_text" , "new_text" ],
261+ },
262+ },
263+ {
264+ "name" : "background_run" ,
265+ "description" : "Run command in background thread. Returns task_id immediately." ,
266+ "input_schema" : {
267+ "type" : "object" ,
268+ "properties" : {"command" : {"type" : "string" }},
269+ "required" : ["command" ],
270+ },
271+ },
272+ {
273+ "name" : "check_background" ,
274+ "description" : "Check background task status. Omit task_id to list all." ,
275+ "input_schema" : {
276+ "type" : "object" ,
277+ "properties" : {"task_id" : {"type" : "string" }},
278+ },
279+ },
185280]
186281
187282
188283def agent_loop (messages : list ):
189284 while True :
190285 # Drain background notifications and inject as system message before LLM call
191- notifs = BG .drain_notifications ()
192- if notifs and messages :
193- notif_text = "\n " .join (
194- f"[bg:{ n ['task_id' ]} ] { n ['status' ]} : { n ['result' ]} " for n in notifs
195- )
196- messages .append ({"role" : "user" , "content" : f"<background-results>\n { notif_text } \n </background-results>" })
197- messages .append ({"role" : "assistant" , "content" : "Noted background results." })
286+ inject_background_results (messages , BG .drain_notifications ())
198287 response = client .messages .create (
199- model = MODEL , system = SYSTEM , messages = messages ,
200- tools = TOOLS , max_tokens = 8000 ,
288+ model = MODEL ,
289+ system = SYSTEM ,
290+ messages = messages ,
291+ tools = TOOLS ,
292+ max_tokens = 8000 ,
201293 )
202294 messages .append ({"role" : "assistant" , "content" : response .content })
203295 if response .stop_reason != "tool_use" :
296+ if BG .has_running_tasks () and inject_background_results (
297+ messages , BG .wait_for_notifications ()
298+ ):
299+ continue
204300 return
205301 results = []
206302 for block in response .content :
207303 if block .type == "tool_use" :
208304 handler = TOOL_HANDLERS .get (block .name )
209305 try :
210- output = handler (** block .input ) if handler else f"Unknown tool: { block .name } "
306+ output = (
307+ handler (** block .input )
308+ if handler
309+ else f"Unknown tool: { block .name } "
310+ )
211311 except Exception as e :
212312 output = f"Error: { e } "
213313 print (f"> { block .name } : { str (output )[:200 ]} " )
214- results .append ({"type" : "tool_result" , "tool_use_id" : block .id , "content" : str (output )})
314+ results .append (
315+ {
316+ "type" : "tool_result" ,
317+ "tool_use_id" : block .id ,
318+ "content" : str (output ),
319+ }
320+ )
215321 messages .append ({"role" : "user" , "content" : results })
216322
217323
0 commit comments