mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-03-22 10:25:41 +08:00
188 lines
6.2 KiB
Markdown
188 lines
6.2 KiB
Markdown
# s08: Background Tasks
|
|
|
|
> A BackgroundManager runs commands in separate threads and drains a notification queue before each LLM call, so the agent never blocks on long-running operations.
|
|
|
|
## The Problem
|
|
|
|
Some commands take minutes: `npm install`, `pytest`, `docker build`. With
|
|
a blocking agent loop, the model sits idle waiting for the subprocess to
|
|
finish. It cannot do anything else. If the user asked "install dependencies
|
|
and while that runs, create the config file," the agent would install
|
|
first, _then_ create the config -- sequentially, not in parallel.
|
|
|
|
The agent needs concurrency. Not full multi-threading of the agent loop
|
|
itself, but the ability to fire off a long command and continue working
|
|
while it runs. When the command finishes, its result should appear
|
|
naturally in the conversation.
|
|
|
|
The solution is a BackgroundManager that runs commands in daemon threads
|
|
and collects results in a notification queue. Before each LLM call, the
|
|
queue is drained and results are injected into the messages.
|
|
|
|
## The Solution
|
|
|
|
```
|
|
Main thread Background thread
|
|
+-----------------+ +-----------------+
|
|
| agent loop | | task executes |
|
|
| ... | | ... |
|
|
| [LLM call] <---+------- | enqueue(result) |
|
|
| ^drain queue | +-----------------+
|
|
+-----------------+
|
|
|
|
Timeline:
|
|
Agent --[spawn A]--[spawn B]--[other work]----
|
|
| |
|
|
v v
|
|
[A runs] [B runs] (parallel)
|
|
| |
|
|
+-- notification queue --+
|
|
|
|
|
[results injected before
|
|
next LLM call]
|
|
```
|
|
|
|
## How It Works
|
|
|
|
1. The BackgroundManager tracks tasks and maintains a thread-safe
|
|
notification queue.
|
|
|
|
```python
|
|
class BackgroundManager:
|
|
def __init__(self):
|
|
self.tasks = {}
|
|
self._notification_queue = []
|
|
self._lock = threading.Lock()
|
|
```
|
|
|
|
2. `run()` starts a daemon thread and returns a task_id immediately.
|
|
|
|
```python
|
|
def run(self, command: str) -> str:
|
|
task_id = str(uuid.uuid4())[:8]
|
|
self.tasks[task_id] = {
|
|
"status": "running",
|
|
"result": None,
|
|
"command": command,
|
|
}
|
|
thread = threading.Thread(
|
|
target=self._execute,
|
|
args=(task_id, command),
|
|
daemon=True,
|
|
)
|
|
thread.start()
|
|
return f"Background task {task_id} started"
|
|
```
|
|
|
|
3. The thread target `_execute` runs the subprocess and pushes
|
|
results to the notification queue.
|
|
|
|
```python
|
|
def _execute(self, task_id: str, command: str):
|
|
try:
|
|
r = subprocess.run(command, shell=True, cwd=WORKDIR,
|
|
capture_output=True, text=True, timeout=300)
|
|
output = (r.stdout + r.stderr).strip()[:50000]
|
|
status = "completed"
|
|
except subprocess.TimeoutExpired:
|
|
output = "Error: Timeout (300s)"
|
|
status = "timeout"
|
|
self.tasks[task_id]["status"] = status
|
|
self.tasks[task_id]["result"] = output
|
|
with self._lock:
|
|
self._notification_queue.append({
|
|
"task_id": task_id,
|
|
"status": status,
|
|
"result": output[:500],
|
|
})
|
|
```
|
|
|
|
4. `drain_notifications()` returns and clears pending results.
|
|
|
|
```python
|
|
def drain_notifications(self) -> list:
|
|
with self._lock:
|
|
notifs = list(self._notification_queue)
|
|
self._notification_queue.clear()
|
|
return notifs
|
|
```
|
|
|
|
5. The agent loop drains notifications before each LLM call.
|
|
|
|
```python
|
|
def agent_loop(messages: list):
|
|
while True:
|
|
notifs = BG.drain_notifications()
|
|
if notifs and messages:
|
|
notif_text = "\n".join(
|
|
f"[bg:{n['task_id']}] {n['status']}: "
|
|
f"{n['result']}" for n in notifs
|
|
)
|
|
messages.append({"role": "user",
|
|
"content": f"<background-results>"
|
|
f"\n{notif_text}\n"
|
|
f"</background-results>"})
|
|
messages.append({"role": "assistant",
|
|
"content": "Noted background results."})
|
|
response = client.messages.create(...)
|
|
```
|
|
|
|
## Key Code
|
|
|
|
The BackgroundManager (from `agents/s08_background_tasks.py`, lines 49-107):
|
|
|
|
```python
|
|
class BackgroundManager:
|
|
def __init__(self):
|
|
self.tasks = {}
|
|
self._notification_queue = []
|
|
self._lock = threading.Lock()
|
|
|
|
def run(self, command: str) -> str:
|
|
task_id = str(uuid.uuid4())[:8]
|
|
self.tasks[task_id] = {"status": "running",
|
|
"result": None,
|
|
"command": command}
|
|
thread = threading.Thread(
|
|
target=self._execute,
|
|
args=(task_id, command), daemon=True)
|
|
thread.start()
|
|
return f"Background task {task_id} started"
|
|
|
|
def _execute(self, task_id, command):
|
|
# run subprocess, push to queue
|
|
...
|
|
|
|
def drain_notifications(self) -> list:
|
|
with self._lock:
|
|
notifs = list(self._notification_queue)
|
|
self._notification_queue.clear()
|
|
return notifs
|
|
```
|
|
|
|
## What Changed From s07
|
|
|
|
| Component | Before (s07) | After (s08) |
|
|
|----------------|------------------|----------------------------|
|
|
| Tools | 8 | 6 (base + background_run + check)|
|
|
| Execution | Blocking only | Blocking + background threads|
|
|
| Notification | None | Queue drained per loop |
|
|
| Concurrency | None | Daemon threads |
|
|
|
|
## Design Rationale
|
|
|
|
The agent loop is inherently single-threaded (one LLM call at a time). Background threads break this constraint for I/O-bound work (tests, builds, installs). The notification queue pattern ("drain before next LLM call") ensures results arrive at natural conversation breakpoints rather than interrupting the model's reasoning mid-thought. This is a minimal concurrency model: the agent loop stays single-threaded and deterministic, while only the I/O-bound subprocess execution is parallelized.
|
|
|
|
## Try It
|
|
|
|
```sh
|
|
cd learn-claude-code
|
|
python agents/s08_background_tasks.py
|
|
```
|
|
|
|
Example prompts to try:
|
|
|
|
1. `Run "sleep 5 && echo done" in the background, then create a file while it runs`
|
|
2. `Start 3 background tasks: "sleep 2", "sleep 4", "sleep 6". Check their status.`
|
|
3. `Run pytest in the background and keep working on other things`
|