mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-09-21 21:03:38 +08:00
Refine course progression and runtime safety
This commit is contained in:
@@ -33,7 +33,7 @@ Agent の bash ツールも同じ。`pip install torch` は 10 分、`npm run bu
|
||||
| 遅い操作 | Agent が待機 | バックグラウンドスレッドで実行 |
|
||||
| Agent アイドル | はい | いいえ、処理を継続 |
|
||||
| 結果 | 即時返却 | 次ターンで通知を注入 |
|
||||
| 判断基準 | — | `run_in_background` パラメータ(モデル明示的リクエスト)、ヒューリスティックフォールバック |
|
||||
| 判断基準 | — | bash の `run_in_background` パラメータ、ヒューリスティックフォールバック |
|
||||
|
||||
---
|
||||
|
||||
@@ -41,7 +41,7 @@ Agent の bash ツールも同じ。`pip install torch` は 10 分、`npm run bu
|
||||
|
||||
### should_run_background: 明示的リクエスト優先、ヒューリスティックフォールバック
|
||||
|
||||
モデルは bash ツールの `run_in_background` パラメータで明示的にバックグラウンド実行をリクエストする。指定がない場合は、キーワードヒューリスティックで判断する:
|
||||
モデルは bash ツールの `run_in_background` パラメータで明示的にバックグラウンド実行をリクエストする。指定がない場合は、キーワードヒューリスティックで判断する。この経路に入るのは bash だけであり、他のツールは従来どおり引数を検証して実行する:
|
||||
|
||||
```python
|
||||
def is_slow_operation(tool_name: str, tool_input: dict) -> bool:
|
||||
@@ -56,7 +56,9 @@ def is_slow_operation(tool_name: str, tool_input: dict) -> bool:
|
||||
|
||||
def should_run_background(tool_name: str, tool_input: dict) -> bool:
|
||||
"""Model explicit request takes priority; fallback to heuristic."""
|
||||
if tool_input.get("run_in_background"):
|
||||
if tool_name != "bash":
|
||||
return False
|
||||
if tool_input.get("run_in_background") is True:
|
||||
return True
|
||||
return is_slow_operation(tool_name, tool_input)
|
||||
```
|
||||
@@ -78,9 +80,14 @@ def start_background_task(block) -> str:
|
||||
bg_id = f"bg_{_bg_counter:04d}"
|
||||
|
||||
def worker():
|
||||
result = execute_tool(block)
|
||||
try:
|
||||
output, exit_code = _run_bash_process(block.input["command"])
|
||||
status = "completed" if exit_code == 0 else "failed"
|
||||
result = _format_bash_result(output, exit_code)
|
||||
except Exception as exc:
|
||||
status, result = "failed", f"Error: {exc}"
|
||||
with background_lock:
|
||||
background_tasks[bg_id]["status"] = "completed"
|
||||
background_tasks[bg_id]["status"] = status
|
||||
background_results[bg_id] = result
|
||||
|
||||
with background_lock:
|
||||
@@ -94,7 +101,7 @@ def start_background_task(block) -> str:
|
||||
return bg_id
|
||||
```
|
||||
|
||||
`start_background_task()` は `bg_id` を返す。`daemon=True` により、Agent プロセスの終了時にスレッドも終了する。
|
||||
`start_background_task()` は `bg_id` を返す。command が非ゼロで終了した場合や worker で例外が起きた場合は `failed` となり、成功として扱わない。Shell は独立した process group で起動し、command の完了、timeout、または Agent が通常経路や `SIGTERM` で終了する時に元の group を停止する。これは lifecycle cleanup であって sandbox ではなく、別の session を作った process は group から離れられる。
|
||||
|
||||
### collect_background_results: 通知収集
|
||||
|
||||
@@ -102,10 +109,10 @@ def start_background_task(block) -> str:
|
||||
|
||||
```python
|
||||
def collect_background_results() -> list[str]:
|
||||
"""Collect completed results as task_notification messages."""
|
||||
"""Collect terminal results as task_notification messages."""
|
||||
with background_lock:
|
||||
ready_ids = [bid for bid, task in background_tasks.items()
|
||||
if task["status"] == "completed"]
|
||||
if task["status"] in ("completed", "failed")]
|
||||
notifications = []
|
||||
for bg_id in ready_ids:
|
||||
with background_lock:
|
||||
@@ -114,7 +121,7 @@ def collect_background_results() -> list[str]:
|
||||
notifications.append(
|
||||
f"<task_notification>\n"
|
||||
f" <task_id>{bg_id}</task_id>\n"
|
||||
f" <status>completed</status>\n"
|
||||
f" <status>{task['status']}</status>\n"
|
||||
f" <command>{task['command']}</command>\n"
|
||||
f" <summary>{output[:200]}</summary>\n"
|
||||
f"</task_notification>")
|
||||
@@ -213,4 +220,4 @@ python s13_background_tasks/code.py
|
||||
s14 Cron Scheduler → Agent にアラームクロックを付ける。
|
||||
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||
<!-- translation-sync: zh@v4, en@v4, ja@v4 -->
|
||||
|
||||
@@ -33,7 +33,7 @@ Sync vs Background:
|
||||
| Slow operations | Agent waits | Background thread executes |
|
||||
| Agent idle | Yes | No, continues processing |
|
||||
| Result | Immediate return | Notification injected next turn |
|
||||
| Decision criteria | — | `run_in_background` param (model explicit request), heuristic fallback |
|
||||
| Decision criteria | — | bash `run_in_background` param, heuristic fallback |
|
||||
|
||||
---
|
||||
|
||||
@@ -41,7 +41,7 @@ Sync vs Background:
|
||||
|
||||
### should_run_background: Explicit Request First, Heuristic Fallback
|
||||
|
||||
The model explicitly requests background execution via the bash tool's `run_in_background` parameter. If the model does not specify it, keyword heuristics decide:
|
||||
The model explicitly requests background execution via the bash tool's `run_in_background` parameter. If the model does not specify it, keyword heuristics decide. Only bash enters this path; other tools still run through their normal argument validation.
|
||||
|
||||
```python
|
||||
def is_slow_operation(tool_name: str, tool_input: dict) -> bool:
|
||||
@@ -56,7 +56,9 @@ def is_slow_operation(tool_name: str, tool_input: dict) -> bool:
|
||||
|
||||
def should_run_background(tool_name: str, tool_input: dict) -> bool:
|
||||
"""Model explicit request takes priority; fallback to heuristic."""
|
||||
if tool_input.get("run_in_background"):
|
||||
if tool_name != "bash":
|
||||
return False
|
||||
if tool_input.get("run_in_background") is True:
|
||||
return True
|
||||
return is_slow_operation(tool_name, tool_input)
|
||||
```
|
||||
@@ -78,9 +80,14 @@ def start_background_task(block) -> str:
|
||||
bg_id = f"bg_{_bg_counter:04d}"
|
||||
|
||||
def worker():
|
||||
result = execute_tool(block)
|
||||
try:
|
||||
output, exit_code = _run_bash_process(block.input["command"])
|
||||
status = "completed" if exit_code == 0 else "failed"
|
||||
result = _format_bash_result(output, exit_code)
|
||||
except Exception as exc:
|
||||
status, result = "failed", f"Error: {exc}"
|
||||
with background_lock:
|
||||
background_tasks[bg_id]["status"] = "completed"
|
||||
background_tasks[bg_id]["status"] = status
|
||||
background_results[bg_id] = result
|
||||
|
||||
with background_lock:
|
||||
@@ -94,7 +101,7 @@ def start_background_task(block) -> str:
|
||||
return bg_id
|
||||
```
|
||||
|
||||
`start_background_task()` returns `bg_id`. `daemon=True` ensures the thread exits with the agent process.
|
||||
`start_background_task()` returns `bg_id`. A non-zero exit code or worker exception becomes `failed`, instead of being reported as a successful completion. The shell starts in its own process group. When the command finishes, times out, or the Agent exits through the normal or `SIGTERM` path, the runtime stops that original group. This is lifecycle cleanup, not a sandbox: a process that creates another session can leave the group.
|
||||
|
||||
### collect_background_results: Notification Collection
|
||||
|
||||
@@ -102,10 +109,10 @@ When background tasks complete, results are collected and formatted as `<task_no
|
||||
|
||||
```python
|
||||
def collect_background_results() -> list[str]:
|
||||
"""Collect completed results as task_notification messages."""
|
||||
"""Collect terminal results as task_notification messages."""
|
||||
with background_lock:
|
||||
ready_ids = [bid for bid, task in background_tasks.items()
|
||||
if task["status"] == "completed"]
|
||||
if task["status"] in ("completed", "failed")]
|
||||
notifications = []
|
||||
for bg_id in ready_ids:
|
||||
with background_lock:
|
||||
@@ -114,7 +121,7 @@ def collect_background_results() -> list[str]:
|
||||
notifications.append(
|
||||
f"<task_notification>\n"
|
||||
f" <task_id>{bg_id}</task_id>\n"
|
||||
f" <status>completed</status>\n"
|
||||
f" <status>{task['status']}</status>\n"
|
||||
f" <command>{task['command']}</command>\n"
|
||||
f" <summary>{output[:200]}</summary>\n"
|
||||
f"</task_notification>")
|
||||
@@ -213,4 +220,4 @@ Background tasks solved "slow operations don't block." But what if you want to d
|
||||
s14 Cron Scheduler → Give the agent an alarm clock.
|
||||
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||
<!-- translation-sync: zh@v4, en@v4, ja@v4 -->
|
||||
|
||||
@@ -33,7 +33,7 @@ Agent 的 bash 工具也一样。`pip install torch` 要 10 分钟,`npm run bu
|
||||
| 慢操作 | Agent 干等 | 后台线程执行 |
|
||||
| Agent 空闲 | 是 | 否,继续处理 |
|
||||
| 结果 | 立即返回 | 下轮注入通知 |
|
||||
| 判断标准 | — | `run_in_background` 参数(模型显式请求),启发式兜底 |
|
||||
| 判断标准 | — | bash 的 `run_in_background` 参数,启发式兜底 |
|
||||
|
||||
---
|
||||
|
||||
@@ -41,7 +41,7 @@ Agent 的 bash 工具也一样。`pip install torch` 要 10 分钟,`npm run bu
|
||||
|
||||
### should_run_background: 显式请求优先,启发式兜底
|
||||
|
||||
模型通过 bash 工具的 `run_in_background` 参数显式请求后台执行。如果模型没有指定,则使用关键词启发式判断:
|
||||
模型通过 bash 工具的 `run_in_background` 参数显式请求后台执行。如果模型没有指定,则使用关键词启发式判断。只有 bash 会进入这条路径,其他工具仍按原来的参数规则校验和执行。
|
||||
|
||||
```python
|
||||
def is_slow_operation(tool_name: str, tool_input: dict) -> bool:
|
||||
@@ -56,7 +56,9 @@ def is_slow_operation(tool_name: str, tool_input: dict) -> bool:
|
||||
|
||||
def should_run_background(tool_name: str, tool_input: dict) -> bool:
|
||||
"""Model explicit request takes priority; fallback to heuristic."""
|
||||
if tool_input.get("run_in_background"):
|
||||
if tool_name != "bash":
|
||||
return False
|
||||
if tool_input.get("run_in_background") is True:
|
||||
return True
|
||||
return is_slow_operation(tool_name, tool_input)
|
||||
```
|
||||
@@ -78,9 +80,14 @@ def start_background_task(block) -> str:
|
||||
bg_id = f"bg_{_bg_counter:04d}"
|
||||
|
||||
def worker():
|
||||
result = execute_tool(block)
|
||||
try:
|
||||
output, exit_code = _run_bash_process(block.input["command"])
|
||||
status = "completed" if exit_code == 0 else "failed"
|
||||
result = _format_bash_result(output, exit_code)
|
||||
except Exception as exc:
|
||||
status, result = "failed", f"Error: {exc}"
|
||||
with background_lock:
|
||||
background_tasks[bg_id]["status"] = "completed"
|
||||
background_tasks[bg_id]["status"] = status
|
||||
background_results[bg_id] = result
|
||||
|
||||
with background_lock:
|
||||
@@ -94,7 +101,7 @@ def start_background_task(block) -> str:
|
||||
return bg_id
|
||||
```
|
||||
|
||||
`start_background_task()` 返回 `bg_id`。`daemon=True` 确保 Agent 进程退出时线程一起退出。
|
||||
`start_background_task()` 返回 `bg_id`。命令以非零状态退出或 worker 抛出异常时,任务会进入 `failed`,不会再被写成成功完成。Shell 会在独立的进程组中启动;命令完成、超时,或 Agent 经正常路径、`SIGTERM` 退出时,运行时会停止原进程组。这只是生命周期清理,并不是沙箱;另建 session 的进程仍可能离开该进程组。
|
||||
|
||||
### collect_background_results: 通知收集
|
||||
|
||||
@@ -102,10 +109,10 @@ def start_background_task(block) -> str:
|
||||
|
||||
```python
|
||||
def collect_background_results() -> list[str]:
|
||||
"""Collect completed results as task_notification messages."""
|
||||
"""Collect terminal results as task_notification messages."""
|
||||
with background_lock:
|
||||
ready_ids = [bid for bid, task in background_tasks.items()
|
||||
if task["status"] == "completed"]
|
||||
if task["status"] in ("completed", "failed")]
|
||||
notifications = []
|
||||
for bg_id in ready_ids:
|
||||
with background_lock:
|
||||
@@ -114,7 +121,7 @@ def collect_background_results() -> list[str]:
|
||||
notifications.append(
|
||||
f"<task_notification>\n"
|
||||
f" <task_id>{bg_id}</task_id>\n"
|
||||
f" <status>completed</status>\n"
|
||||
f" <status>{task['status']}</status>\n"
|
||||
f" <command>{task['command']}</command>\n"
|
||||
f" <summary>{output[:200]}</summary>\n"
|
||||
f"</task_notification>")
|
||||
@@ -213,4 +220,4 @@ python s13_background_tasks/code.py
|
||||
s14 Cron Scheduler → 给 Agent 装一个闹钟。
|
||||
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||
<!-- translation-sync: zh@v4, en@v4, ja@v4 -->
|
||||
|
||||
@@ -20,7 +20,7 @@ This chapter keeps the agent loop focused on background tasks. Error recovery
|
||||
remains the independent layer introduced in s11.
|
||||
"""
|
||||
|
||||
import os, subprocess, json, time, random, threading
|
||||
import atexit, os, signal, subprocess, json, time, random, threading
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass, asdict
|
||||
|
||||
@@ -180,15 +180,77 @@ def safe_path(p: str) -> Path:
|
||||
return path
|
||||
|
||||
|
||||
_shell_processes: set[subprocess.Popen] = set()
|
||||
_shell_process_lock = threading.RLock()
|
||||
|
||||
|
||||
def _stop_process_group(process: subprocess.Popen):
|
||||
"""Stop processes that remain in the command's original process group."""
|
||||
for sig in (signal.SIGTERM, signal.SIGKILL):
|
||||
try:
|
||||
os.killpg(process.pid, sig)
|
||||
except ProcessLookupError:
|
||||
return
|
||||
except OSError:
|
||||
return
|
||||
time.sleep(0.05)
|
||||
|
||||
|
||||
def _stop_all_shell_processes():
|
||||
with _shell_process_lock:
|
||||
processes = list(_shell_processes)
|
||||
for process in processes:
|
||||
_stop_process_group(process)
|
||||
|
||||
|
||||
def _handle_termination_signal(signum, _frame):
|
||||
_stop_all_shell_processes()
|
||||
raise SystemExit(128 + signum)
|
||||
|
||||
|
||||
atexit.register(_stop_all_shell_processes)
|
||||
signal.signal(signal.SIGTERM, _handle_termination_signal)
|
||||
|
||||
|
||||
def _run_bash_process(command: str, cwd: Path | None = None) -> tuple[str, int | None]:
|
||||
process = None
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
command, shell=True, cwd=cwd or WORKDIR,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
text=True, start_new_session=True,
|
||||
)
|
||||
with _shell_process_lock:
|
||||
_shell_processes.add(process)
|
||||
stdout, stderr = process.communicate(timeout=120)
|
||||
out = (stdout + stderr).strip()
|
||||
return (out[:50000] if out else "(no output)"), process.returncode
|
||||
except subprocess.TimeoutExpired:
|
||||
return "Error: Timeout (120s)", None
|
||||
except OSError as exc:
|
||||
return f"Error: {type(exc).__name__}: {exc}", None
|
||||
finally:
|
||||
if process is not None:
|
||||
_stop_process_group(process)
|
||||
try:
|
||||
process.wait(timeout=0.2)
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
with _shell_process_lock:
|
||||
_shell_processes.discard(process)
|
||||
|
||||
|
||||
def _format_bash_result(output: str, exit_code: int | None) -> str:
|
||||
if exit_code == 0:
|
||||
return output
|
||||
if exit_code is None:
|
||||
return output
|
||||
return f"Error: command exited with status {exit_code}\n{output}"
|
||||
|
||||
|
||||
def run_bash(command: str, run_in_background: bool = False) -> str:
|
||||
# run_in_background is handled by agent_loop dispatch, not here
|
||||
try:
|
||||
r = subprocess.run(command, shell=True, cwd=WORKDIR,
|
||||
capture_output=True, text=True, timeout=120)
|
||||
out = (r.stdout + r.stderr).strip()
|
||||
return out[:50000] if out else "(no output)"
|
||||
except subprocess.TimeoutExpired:
|
||||
return "Error: Timeout (120s)"
|
||||
return _format_bash_result(*_run_bash_process(command))
|
||||
|
||||
|
||||
def run_read(path: str, limit: int | None = None) -> str:
|
||||
@@ -327,30 +389,42 @@ def is_slow_operation(tool_name: str, tool_input: dict) -> bool:
|
||||
|
||||
def should_run_background(tool_name: str, tool_input: dict) -> bool:
|
||||
"""Model explicit request takes priority; fallback to heuristic."""
|
||||
if tool_input.get("run_in_background"):
|
||||
return True
|
||||
return is_slow_operation(tool_name, tool_input)
|
||||
return tool_name == "bash" and (
|
||||
tool_input.get("run_in_background") is True
|
||||
or is_slow_operation(tool_name, tool_input)
|
||||
)
|
||||
|
||||
|
||||
def execute_tool(block) -> str:
|
||||
"""Execute a tool call block, return output."""
|
||||
handler = TOOL_HANDLERS.get(block.name)
|
||||
if handler:
|
||||
return handler(**block.input)
|
||||
return f"Unknown tool: {block.name}"
|
||||
if not handler:
|
||||
return f"Unknown tool: {block.name}"
|
||||
try:
|
||||
return str(handler(**block.input))
|
||||
except (TypeError, ValueError) as exc:
|
||||
return f"Error: {exc}"
|
||||
|
||||
|
||||
def start_background_task(block) -> str:
|
||||
"""Run tool in a daemon thread. Returns background task ID."""
|
||||
"""Run one bash call in a daemon thread. Returns background task ID."""
|
||||
global _bg_counter
|
||||
_bg_counter += 1
|
||||
bg_id = f"bg_{_bg_counter:04d}"
|
||||
cmd = block.input.get("command", block.name)
|
||||
|
||||
def worker():
|
||||
result = execute_tool(block)
|
||||
try:
|
||||
if block.name != "bash":
|
||||
raise ValueError("only bash can run in the background")
|
||||
output, exit_code = _run_bash_process(str(block.input["command"]))
|
||||
result = _format_bash_result(output, exit_code)
|
||||
status = "completed" if exit_code == 0 else "failed"
|
||||
except Exception as exc:
|
||||
result = f"Error: {type(exc).__name__}: {exc}"
|
||||
status = "failed"
|
||||
with background_lock:
|
||||
background_tasks[bg_id]["status"] = "completed"
|
||||
background_tasks[bg_id]["status"] = status
|
||||
background_results[bg_id] = result
|
||||
|
||||
with background_lock:
|
||||
@@ -366,10 +440,10 @@ def start_background_task(block) -> str:
|
||||
|
||||
|
||||
def collect_background_results() -> list[str]:
|
||||
"""Collect completed background results as task_notification messages."""
|
||||
"""Collect terminal background results as task_notification messages."""
|
||||
with background_lock:
|
||||
ready_ids = [bid for bid, task in background_tasks.items()
|
||||
if task["status"] == "completed"]
|
||||
if task["status"] in {"completed", "failed"}]
|
||||
notifications = []
|
||||
for bg_id in ready_ids:
|
||||
with background_lock:
|
||||
@@ -379,7 +453,7 @@ def collect_background_results() -> list[str]:
|
||||
notifications.append(
|
||||
f"<task_notification>\n"
|
||||
f" <task_id>{bg_id}</task_id>\n"
|
||||
f" <status>completed</status>\n"
|
||||
f" <status>{task['status']}</status>\n"
|
||||
f" <command>{task['command']}</command>\n"
|
||||
f" <summary>{summary}</summary>\n"
|
||||
f"</task_notification>")
|
||||
|
||||
Reference in New Issue
Block a user