Skip to content

Commit 92c5359

Browse files
authored
Merge pull request #6 from ynlai1982/feature/agent-parameter
feat: add --openclaw-agent flag to spawn command
2 parents 9ba95ca + 61cbff8 commit 92c5359

5 files changed

Lines changed: 143 additions & 1 deletion

File tree

clawteam/cli/commands.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1727,7 +1727,7 @@ def _fmt(d: dict) -> None:
17271727
@app.command("spawn")
17281728
def spawn_agent(
17291729
backend: Optional[str] = typer.Argument(None, help="Backend: tmux (default) or subprocess"),
1730-
command: list[str] = typer.Argument(None, help="Command and arguments to run (default: claude)"),
1730+
command: list[str] = typer.Argument(None, help="Command and arguments to run (default: openclaw)"),
17311731
team: Optional[str] = typer.Option(None, "--team", "-t", help="Team name"),
17321732
agent_name: Optional[str] = typer.Option(None, "--agent-name", "-n", help="Agent name"),
17331733
agent_type: str = typer.Option("general-purpose", "--agent-type", help="Agent type"),
@@ -1736,6 +1736,7 @@ def spawn_agent(
17361736
repo: Optional[str] = typer.Option(None, "--repo", help="Git repo path (default: cwd)"),
17371737
skip_permissions: Optional[bool] = typer.Option(None, "--skip-permissions/--no-skip-permissions", help="Skip tool approval for claude (default: from config, true)"),
17381738
resume: bool = typer.Option(False, "--resume", "-r", help="Resume previous session if available"),
1739+
openclaw_agent: Optional[str] = typer.Option(None, "--openclaw-agent", help="OpenClaw agent id to use (routes to a specific agent config/model)"),
17391740
):
17401741
"""Spawn a new agent process with identity + task as its initial prompt.
17411742
@@ -1852,6 +1853,7 @@ def spawn_agent(
18521853
prompt=prompt,
18531854
cwd=cwd,
18541855
skip_permissions=skip_permissions,
1856+
openclaw_agent=openclaw_agent,
18551857
)
18561858

18571859
if result.startswith("Error"):

clawteam/spawn/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ def spawn(
2020
env: dict[str, str] | None = None,
2121
cwd: str | None = None,
2222
skip_permissions: bool = False,
23+
openclaw_agent: str | None = None,
2324
) -> str:
2425
"""Spawn a new agent process. Returns a status message."""
2526

clawteam/spawn/subprocess_backend.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,14 @@ def spawn(
4040
env: dict[str, str] | None = None,
4141
cwd: str | None = None,
4242
skip_permissions: bool = False,
43+
openclaw_agent: str | None = None,
4344
) -> str:
45+
if openclaw_agent:
46+
raise NotImplementedError(
47+
f"openclaw_agent is not supported with subprocess backend "
48+
f"(got {openclaw_agent!r}); use tmux backend instead."
49+
)
50+
4451
spawn_env = os.environ.copy()
4552
clawteam_bin = resolve_clawteam_executable()
4653
spawn_env.update({

clawteam/spawn/tmux_backend.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import shlex
88
import shutil
99
import subprocess
10+
import sys
1011
import tempfile
1112
import time
1213

@@ -50,10 +51,19 @@ def spawn(
5051
env: dict[str, str] | None = None,
5152
cwd: str | None = None,
5253
skip_permissions: bool = False,
54+
openclaw_agent: str | None = None,
5355
) -> str:
5456
if not shutil.which("tmux"):
5557
return "Error: tmux not installed"
5658

59+
if openclaw_agent:
60+
print(
61+
f"Warning: openclaw_agent={openclaw_agent!r} requires openclaw tui to support "
62+
"the --agent parameter (see openclaw/openclaw#51481). "
63+
"This flag may have no effect if your openclaw version does not support it.",
64+
file=sys.stderr,
65+
)
66+
5767
session_name = f"clawteam-{team_name}"
5868
clawteam_bin = resolve_clawteam_executable()
5969
env_vars = os.environ.copy()
@@ -114,13 +124,19 @@ def spawn(
114124
session_key = f"clawteam-{team_name}-{agent_name}"
115125
if final_command[0].endswith("openclaw") and len(final_command) == 1:
116126
final_command = [final_command[0], "tui", "--session", session_key]
127+
if openclaw_agent:
128+
final_command.extend(["--agent", openclaw_agent])
117129
if prompt:
118130
final_command.extend(["--message", prompt])
119131
elif "tui" in final_command:
120132
final_command.extend(["--session", session_key])
133+
if openclaw_agent:
134+
final_command.extend(["--agent", openclaw_agent])
121135
if prompt:
122136
final_command.extend(["--message", prompt])
123137
elif "agent" in final_command:
138+
if openclaw_agent:
139+
final_command.extend(["--agent", openclaw_agent])
124140
if prompt:
125141
final_command.extend(["--message", prompt])
126142

tests/test_openclaw_agent.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""Tests for openclaw_agent parameter handling in spawn backends."""
2+
3+
from __future__ import annotations
4+
5+
from unittest.mock import MagicMock
6+
7+
8+
# ---------------------------------------------------------------------------
9+
# TmuxBackend tests
10+
# ---------------------------------------------------------------------------
11+
12+
def _make_tmux_mocks(monkeypatch, captured: dict, *, tmux_ok: bool = True):
13+
"""Patch tmux, shutil.which, register_agent, and time.sleep for TmuxBackend tests."""
14+
monkeypatch.setattr("clawteam.spawn.tmux_backend.shutil.which", lambda name: "/usr/bin/tmux" if name == "tmux" else None)
15+
monkeypatch.setattr("clawteam.spawn.command_validation.shutil.which", lambda name, path=None: f"/usr/bin/{name}")
16+
17+
def fake_run(cmd, **kwargs):
18+
result = MagicMock()
19+
result.returncode = 0
20+
result.stdout = "pane-id\n" if "list-panes" in cmd else b""
21+
result.stderr = b""
22+
captured.setdefault("runs", []).append(cmd)
23+
if "new-session" in cmd or "new-window" in cmd:
24+
# Capture the full command string for assertion
25+
captured["spawn_cmd"] = cmd
26+
return result
27+
28+
monkeypatch.setattr("clawteam.spawn.tmux_backend.subprocess.run", fake_run)
29+
monkeypatch.setattr("clawteam.spawn.tmux_backend.time.sleep", lambda _: None)
30+
monkeypatch.setattr("clawteam.spawn.registry.register_agent", lambda **_: None)
31+
monkeypatch.setattr("clawteam.spawn.tmux_backend._confirm_workspace_trust_if_prompted", lambda *a, **kw: False)
32+
33+
34+
def test_tmux_backend_includes_agent_flag_when_openclaw_agent_set(monkeypatch, capsys):
35+
"""tmux_backend.spawn() with openclaw_agent='researcher' should include --agent researcher in command."""
36+
from clawteam.spawn.tmux_backend import TmuxBackend
37+
38+
captured: dict = {}
39+
_make_tmux_mocks(monkeypatch, captured)
40+
41+
backend = TmuxBackend()
42+
backend.spawn(
43+
command=["openclaw"],
44+
agent_name="researcher",
45+
agent_id="agent-1",
46+
agent_type="general-purpose",
47+
team_name="test-team",
48+
prompt="hello world",
49+
openclaw_agent="researcher",
50+
)
51+
52+
# The spawn command (new-session or new-window) should contain --agent researcher
53+
spawn_cmd = captured.get("spawn_cmd", [])
54+
# The full shell command is the last element in the tmux new-session/new-window call
55+
full_shell_cmd = spawn_cmd[-1] if spawn_cmd else ""
56+
assert "--agent researcher" in full_shell_cmd, (
57+
f"Expected '--agent researcher' in final command, got: {full_shell_cmd!r}"
58+
)
59+
60+
# Warning should be printed to stderr
61+
stderr_output = capsys.readouterr().err
62+
assert "openclaw_agent" in stderr_output or "--agent" in stderr_output
63+
64+
65+
def test_tmux_backend_excludes_agent_flag_when_not_set(monkeypatch):
66+
"""tmux_backend.spawn() without openclaw_agent should not include --agent in the openclaw command."""
67+
from clawteam.spawn.tmux_backend import TmuxBackend
68+
69+
captured: dict = {}
70+
_make_tmux_mocks(monkeypatch, captured)
71+
72+
backend = TmuxBackend()
73+
backend.spawn(
74+
command=["openclaw"],
75+
agent_name="worker",
76+
agent_id="agent-2",
77+
agent_type="general-purpose",
78+
team_name="test-team",
79+
prompt="hello world",
80+
openclaw_agent=None,
81+
)
82+
83+
spawn_cmd = captured.get("spawn_cmd", [])
84+
full_shell_cmd = spawn_cmd[-1] if spawn_cmd else ""
85+
# The exit hook always contains "--agent <name>" for lifecycle; we only want to
86+
# verify the openclaw command itself (before the ";") does not carry --agent.
87+
# Split on ";" to isolate the openclaw command portion.
88+
openclaw_part = full_shell_cmd.split(";")
89+
# Find the segment containing "openclaw tui" (the actual agent command)
90+
openclaw_cmd_segment = next(
91+
(seg for seg in openclaw_part if "openclaw" in seg and "lifecycle" not in seg), ""
92+
)
93+
assert "--agent" not in openclaw_cmd_segment, (
94+
f"Expected no '--agent' in openclaw command segment, got: {openclaw_cmd_segment!r}"
95+
)
96+
97+
98+
# ---------------------------------------------------------------------------
99+
# SubprocessBackend tests
100+
# ---------------------------------------------------------------------------
101+
102+
def test_subprocess_backend_raises_with_openclaw_agent(monkeypatch):
103+
"""subprocess_backend.spawn() with openclaw_agent should raise NotImplementedError."""
104+
import pytest
105+
from clawteam.spawn.subprocess_backend import SubprocessBackend
106+
107+
backend = SubprocessBackend()
108+
with pytest.raises(NotImplementedError, match="subprocess backend"):
109+
backend.spawn(
110+
command=["codex"],
111+
agent_name="worker",
112+
agent_id="agent-3",
113+
agent_type="general-purpose",
114+
team_name="test-team",
115+
openclaw_agent="researcher",
116+
)

0 commit comments

Comments
 (0)