-
Notifications
You must be signed in to change notification settings - Fork 759
Expand file tree
/
Copy pathsubprocess_backend.py
More file actions
124 lines (108 loc) · 4.45 KB
/
Copy pathsubprocess_backend.py
File metadata and controls
124 lines (108 loc) · 4.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
"""Subprocess spawn backend - launches agents as separate processes."""
from __future__ import annotations
import os
import shlex
import subprocess
from clawteam.spawn.adapters import NativeCliAdapter, is_openclaw_command
from clawteam.spawn.base import SpawnBackend
from clawteam.spawn.cli_env import build_spawn_path, resolve_clawteam_executable
from clawteam.spawn.command_validation import validate_spawn_command
class SubprocessBackend(SpawnBackend):
"""Spawn agents as independent subprocesses running any command."""
def __init__(self):
self._processes: dict[str, subprocess.Popen] = {}
self._adapter = NativeCliAdapter()
def spawn(
self,
command: list[str],
agent_name: str,
agent_id: str,
agent_type: str,
team_name: str,
prompt: str | None = None,
env: dict[str, str] | None = None,
cwd: str | None = None,
skip_permissions: bool = False,
) -> str:
from clawteam.team.models import get_data_dir
spawn_env = os.environ.copy()
clawteam_bin = resolve_clawteam_executable()
spawn_env.update({
"CLAWTEAM_AGENT_ID": agent_id,
"CLAWTEAM_AGENT_NAME": agent_name,
"CLAWTEAM_AGENT_TYPE": agent_type,
"CLAWTEAM_TEAM_NAME": team_name,
"CLAWTEAM_AGENT_LEADER": "0",
})
# Propagate resolved data dir so spawned agents find the right
# task/inbox storage even when the leader resolved it via config.
spawn_env.setdefault("CLAWTEAM_DATA_DIR", str(get_data_dir()))
# Propagate user if set
user = os.environ.get("CLAWTEAM_USER", "")
if user:
spawn_env["CLAWTEAM_USER"] = user
# Propagate transport if set
transport = os.environ.get("CLAWTEAM_TRANSPORT", "")
if transport:
spawn_env["CLAWTEAM_TRANSPORT"] = transport
if cwd:
spawn_env["CLAWTEAM_WORKSPACE_DIR"] = cwd
if env:
spawn_env.update(env)
spawn_env["PATH"] = build_spawn_path(spawn_env.get("PATH"))
if os.path.isabs(clawteam_bin):
spawn_env.setdefault("CLAWTEAM_BIN", clawteam_bin)
prepared = self._adapter.prepare_command(
command,
prompt=prompt,
cwd=cwd,
skip_permissions=skip_permissions,
agent_name=agent_name,
interactive=False,
)
normalized_command = prepared.normalized_command
validation_command = normalized_command
final_command = list(prepared.final_command)
# Isolate OpenClaw agents in per-agent sessions
if is_openclaw_command(normalized_command):
session_key = f"clawteam-{team_name}-{agent_name}"
final_command.extend(["--session-id", session_key])
command_error = validate_spawn_command(validation_command, path=spawn_env["PATH"], cwd=cwd)
if command_error:
return command_error
# Wrap with on-exit hook so task status updates immediately on exit
cmd_str = " ".join(shlex.quote(c) for c in final_command)
exit_cmd = shlex.quote(clawteam_bin) if os.path.isabs(clawteam_bin) else "clawteam"
exit_hook = (
f"{exit_cmd} lifecycle on-exit --team {shlex.quote(team_name)} "
f"--agent {shlex.quote(agent_name)}"
)
shell_cmd = f"{cmd_str}; {exit_hook}"
process = subprocess.Popen(
shell_cmd,
shell=True,
env=spawn_env,
# Subprocess agents are fire-and-forget; unread pipes can block long-lived runs.
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
cwd=cwd,
)
self._processes[agent_name] = process
# Persist spawn info for liveness checking
from clawteam.spawn.registry import register_agent
register_agent(
team_name=team_name,
agent_name=agent_name,
backend="subprocess",
pid=process.pid,
command=list(final_command),
)
return f"Agent '{agent_name}' spawned as subprocess (pid={process.pid})"
def list_running(self) -> list[dict[str, str]]:
result = []
for name, proc in list(self._processes.items()):
if proc.poll() is None:
result.append({"name": name, "pid": str(proc.pid), "backend": "subprocess"})
else:
self._processes.pop(name, None)
return result