mirror of
https://github.com/MrFadiAi/openclaw-manager.git
synced 2026-08-14 00:57:59 +00:00
feat: add subagent configuration, default agent toggle, and agent path restructure (v0.0.6)
- Add nested subagent config per agent (allowAgents) with global defaults (maxSpawnDepth, maxChildrenPerAgent, maxConcurrent)
- Add Default Agent toggle in agent form and quick wizard
- Restructure agent paths: workspace-{id}, agents/{id}/agent, agents/{id}/sessions
- Add name/default fields to AgentInfo
- Add Subagent Defaults card with save functionality
- Bump version to 0.0.6
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openclaw-manager",
|
||||
"version": "0.0.5",
|
||||
"version": "0.0.6",
|
||||
"description": "🦞 OpenClaw Cross-Platform Manager - High-Performance AI Assistant Configuration & Service Management",
|
||||
"author": "OpenClaw Team",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "openclaw-manager"
|
||||
version = "0.0.5"
|
||||
version = "0.0.6"
|
||||
description = "OpenClaw Cross-Platform Manager"
|
||||
authors = ["OpenClaw Team"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -2134,6 +2134,25 @@ pub struct AgentInfo {
|
||||
pub sandbox: Option<bool>,
|
||||
pub heartbeat: Option<String>,
|
||||
pub default: Option<bool>,
|
||||
pub subagents: Option<SubagentConfig>,
|
||||
}
|
||||
|
||||
/// Per-agent subagent configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct SubagentConfig {
|
||||
#[serde(alias = "allowAgents", alias = "allow_agents")]
|
||||
pub allow_agents: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Global subagent defaults
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct SubagentDefaults {
|
||||
#[serde(alias = "maxSpawnDepth", alias = "max_spawn_depth")]
|
||||
pub max_spawn_depth: Option<u32>,
|
||||
#[serde(alias = "maxChildrenPerAgent", alias = "max_children_per_agent")]
|
||||
pub max_children_per_agent: Option<u32>,
|
||||
#[serde(alias = "maxConcurrent", alias = "max_concurrent")]
|
||||
pub max_concurrent: Option<u32>,
|
||||
}
|
||||
|
||||
/// Agent binding rule
|
||||
@@ -2158,6 +2177,7 @@ pub struct MatchRule {
|
||||
pub struct AgentsConfigResponse {
|
||||
pub agents: Vec<AgentInfo>,
|
||||
pub bindings: Vec<AgentBinding>,
|
||||
pub subagent_defaults: SubagentDefaults,
|
||||
}
|
||||
|
||||
/// Get multi-agent routing configuration
|
||||
@@ -2182,6 +2202,12 @@ pub async fn get_agents_config() -> Result<AgentsConfigResponse, String> {
|
||||
sandbox: agent_val.get("sandbox").and_then(|v| v.as_bool()),
|
||||
heartbeat: agent_val.pointer("/heartbeat/every").and_then(|v| v.as_str()).map(|s| s.to_string()),
|
||||
default: agent_val.get("default").and_then(|v| v.as_bool()),
|
||||
subagents: agent_val.get("subagents").and_then(|v| {
|
||||
let allow = v.get("allowAgents").and_then(|a| a.as_array()).map(|arr| {
|
||||
arr.iter().filter_map(|x| x.as_str().map(|s| s.to_string())).collect()
|
||||
});
|
||||
Some(SubagentConfig { allow_agents: allow })
|
||||
}),
|
||||
});
|
||||
}
|
||||
} else if let Some(list_obj) = config.pointer("/agents/list").and_then(|v| v.as_object()) {
|
||||
@@ -2196,6 +2222,12 @@ pub async fn get_agents_config() -> Result<AgentsConfigResponse, String> {
|
||||
sandbox: agent_val.get("sandbox").and_then(|v| v.as_bool()),
|
||||
heartbeat: agent_val.pointer("/heartbeat/every").and_then(|v| v.as_str()).map(|s| s.to_string()),
|
||||
default: agent_val.get("default").and_then(|v| v.as_bool()),
|
||||
subagents: agent_val.get("subagents").and_then(|v| {
|
||||
let allow = v.get("allowAgents").and_then(|a| a.as_array()).map(|arr| {
|
||||
arr.iter().filter_map(|x| x.as_str().map(|s| s.to_string())).collect()
|
||||
});
|
||||
Some(SubagentConfig { allow_agents: allow })
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2220,8 +2252,19 @@ pub async fn get_agents_config() -> Result<AgentsConfigResponse, String> {
|
||||
}
|
||||
}
|
||||
|
||||
// Read global subagent defaults from agents.defaults.subagents
|
||||
let subagent_defaults = if let Some(sub_val) = config.pointer("/agents/defaults/subagents") {
|
||||
SubagentDefaults {
|
||||
max_spawn_depth: sub_val.get("maxSpawnDepth").and_then(|v| v.as_u64()).map(|v| v as u32),
|
||||
max_children_per_agent: sub_val.get("maxChildrenPerAgent").and_then(|v| v.as_u64()).map(|v| v as u32),
|
||||
max_concurrent: sub_val.get("maxConcurrent").and_then(|v| v.as_u64()).map(|v| v as u32),
|
||||
}
|
||||
} else {
|
||||
SubagentDefaults::default()
|
||||
};
|
||||
|
||||
info!("[Agents] Found {} agents, {} bindings", agents.len(), bindings.len());
|
||||
Ok(AgentsConfigResponse { agents, bindings })
|
||||
Ok(AgentsConfigResponse { agents, bindings, subagent_defaults })
|
||||
}
|
||||
|
||||
/// Save (add/update) an agent
|
||||
@@ -2270,6 +2313,13 @@ pub async fn save_agent(agent: AgentInfo) -> Result<String, String> {
|
||||
agent_obj["default"] = json!(true);
|
||||
}
|
||||
}
|
||||
if let Some(sub) = &agent.subagents {
|
||||
if let Some(allow) = &sub.allow_agents {
|
||||
if !allow.is_empty() {
|
||||
agent_obj["subagents"] = json!({ "allowAgents": allow });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate legacy object format to array if needed
|
||||
let mut list = if let Some(arr) = config["agents"].get("list").and_then(|v| v.as_array()) {
|
||||
@@ -2426,6 +2476,37 @@ pub async fn save_agent(agent: AgentInfo) -> Result<String, String> {
|
||||
Ok(format!("Agent '{}' saved", agent.id))
|
||||
}
|
||||
|
||||
/// Save global subagent defaults
|
||||
#[command]
|
||||
pub async fn save_subagent_defaults(defaults: SubagentDefaults) -> Result<String, String> {
|
||||
info!("[Agents] Saving subagent defaults");
|
||||
let mut config = load_openclaw_config()?;
|
||||
|
||||
// Ensure agents.defaults exists
|
||||
if config.get("agents").is_none() {
|
||||
config["agents"] = json!({});
|
||||
}
|
||||
if config["agents"].get("defaults").is_none() {
|
||||
config["agents"]["defaults"] = json!({});
|
||||
}
|
||||
|
||||
let mut sub_obj = json!({});
|
||||
if let Some(depth) = defaults.max_spawn_depth {
|
||||
sub_obj["maxSpawnDepth"] = json!(depth);
|
||||
}
|
||||
if let Some(children) = defaults.max_children_per_agent {
|
||||
sub_obj["maxChildrenPerAgent"] = json!(children);
|
||||
}
|
||||
if let Some(concurrent) = defaults.max_concurrent {
|
||||
sub_obj["maxConcurrent"] = json!(concurrent);
|
||||
}
|
||||
|
||||
config["agents"]["defaults"]["subagents"] = sub_obj;
|
||||
|
||||
save_openclaw_config(&config)?;
|
||||
Ok("Subagent defaults saved".to_string())
|
||||
}
|
||||
|
||||
/// Delete an agent
|
||||
#[command]
|
||||
pub async fn delete_agent(agent_id: String) -> Result<String, String> {
|
||||
|
||||
@@ -97,6 +97,7 @@ fn main() {
|
||||
config::get_openclaw_home_dir,
|
||||
config::get_agents_config,
|
||||
config::save_agent,
|
||||
config::save_subagent_defaults,
|
||||
config::delete_agent,
|
||||
config::save_agent_binding,
|
||||
config::delete_agent_binding,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "OpenClaw Manager",
|
||||
"version": "0.0.5",
|
||||
"version": "0.0.6",
|
||||
"identifier": "com.openclaw.manager",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
@@ -25,6 +25,16 @@ import {
|
||||
import { appLogger } from '../../lib/logger';
|
||||
|
||||
// Types corresponding to Rust backend
|
||||
interface SubagentConfig {
|
||||
allow_agents: string[] | null;
|
||||
}
|
||||
|
||||
interface SubagentDefaults {
|
||||
max_spawn_depth: number | null;
|
||||
max_children_per_agent: number | null;
|
||||
max_concurrent: number | null;
|
||||
}
|
||||
|
||||
interface AgentInfo {
|
||||
id: string;
|
||||
name: string | null;
|
||||
@@ -34,6 +44,7 @@ interface AgentInfo {
|
||||
sandbox: boolean | null;
|
||||
heartbeat: string | null;
|
||||
default: boolean | null;
|
||||
subagents: SubagentConfig | null;
|
||||
}
|
||||
|
||||
interface MatchRule {
|
||||
@@ -50,6 +61,7 @@ interface AgentBinding {
|
||||
interface AgentsConfigResponse {
|
||||
agents: AgentInfo[];
|
||||
bindings: AgentBinding[];
|
||||
subagent_defaults: SubagentDefaults;
|
||||
}
|
||||
|
||||
interface TelegramAccount {
|
||||
@@ -103,8 +115,17 @@ export function Agents() {
|
||||
sandbox: null,
|
||||
heartbeat: null,
|
||||
default: null,
|
||||
subagents: null,
|
||||
});
|
||||
|
||||
// Subagent defaults state
|
||||
const [subagentDefaults, setSubagentDefaults] = useState<SubagentDefaults>({
|
||||
max_spawn_depth: null,
|
||||
max_children_per_agent: null,
|
||||
max_concurrent: null,
|
||||
});
|
||||
const [savingDefaults, setSavingDefaults] = useState(false);
|
||||
|
||||
const [bindingForm, setBindingForm] = useState<AgentBinding>({
|
||||
agent_id: '',
|
||||
match_rule: {
|
||||
@@ -131,6 +152,7 @@ export function Agents() {
|
||||
const data = await invoke<AgentsConfigResponse>('get_agents_config');
|
||||
setAgents(data.agents);
|
||||
setBindings(data.bindings);
|
||||
setSubagentDefaults(data.subagent_defaults);
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
appLogger.error('Failed to fetch agents config', e);
|
||||
@@ -238,6 +260,7 @@ export function Agents() {
|
||||
sandbox: agent.sandbox,
|
||||
heartbeat: agent.heartbeat,
|
||||
default: null,
|
||||
subagents: null,
|
||||
});
|
||||
setSystemPrompt('');
|
||||
// Load original system prompt for cloning
|
||||
@@ -284,6 +307,7 @@ export function Agents() {
|
||||
sandbox: null,
|
||||
heartbeat: null,
|
||||
default: wizardForm.isDefault || null,
|
||||
subagents: null,
|
||||
};
|
||||
await invoke('save_agent', { agent });
|
||||
|
||||
@@ -410,7 +434,7 @@ export function Agents() {
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingAgent(null);
|
||||
setAgentForm({ id: '', name: null, workspace: openclawHomeDir || null, agent_dir: null, model: null, sandbox: null, heartbeat: null, default: null });
|
||||
setAgentForm({ id: '', name: null, workspace: openclawHomeDir || null, agent_dir: null, model: null, sandbox: null, heartbeat: null, default: null, subagents: null });
|
||||
setSystemPrompt('');
|
||||
setShowAgentDialog(true);
|
||||
}}
|
||||
@@ -495,6 +519,77 @@ export function Agents() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Subagent Defaults Section */}
|
||||
<section className="bg-dark-800 rounded-xl border border-dark-600 p-5">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-white flex items-center gap-2">
|
||||
<GitMerge className="text-indigo-400" size={24} />
|
||||
Subagent Defaults
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500">Global limits for nested subagent spawning</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={async () => {
|
||||
setSavingDefaults(true);
|
||||
try {
|
||||
await invoke('save_subagent_defaults', { defaults: subagentDefaults });
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
} finally {
|
||||
setSavingDefaults(false);
|
||||
}
|
||||
}}
|
||||
disabled={savingDefaults}
|
||||
className="btn-primary flex items-center gap-2 text-sm"
|
||||
>
|
||||
{savingDefaults ? <Loader2 className="animate-spin" size={14} /> : <Save size={14} />}
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-400 mb-1">Max Spawn Depth</label>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={10}
|
||||
value={subagentDefaults.max_spawn_depth ?? ''}
|
||||
onChange={e => setSubagentDefaults({ ...subagentDefaults, max_spawn_depth: e.target.value ? parseInt(e.target.value) : null })}
|
||||
className="input-base text-center"
|
||||
placeholder="2"
|
||||
/>
|
||||
<p className="text-xs text-gray-600 mt-1">Nesting levels</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-400 mb-1">Max Children / Agent</label>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={50}
|
||||
value={subagentDefaults.max_children_per_agent ?? ''}
|
||||
onChange={e => setSubagentDefaults({ ...subagentDefaults, max_children_per_agent: e.target.value ? parseInt(e.target.value) : null })}
|
||||
className="input-base text-center"
|
||||
placeholder="5"
|
||||
/>
|
||||
<p className="text-xs text-gray-600 mt-1">Per parent</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-400 mb-1">Max Concurrent</label>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
value={subagentDefaults.max_concurrent ?? ''}
|
||||
onChange={e => setSubagentDefaults({ ...subagentDefaults, max_concurrent: e.target.value ? parseInt(e.target.value) : null })}
|
||||
className="input-base text-center"
|
||||
placeholder="8"
|
||||
/>
|
||||
<p className="text-xs text-gray-600 mt-1">System-wide</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Bindings Section */}
|
||||
<section>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
@@ -767,6 +862,45 @@ export function Agents() {
|
||||
placeholder='e.g. "1h" or "0m" to disable'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Subagents — Allow Agents */}
|
||||
<div className="pt-3 border-t border-dark-600">
|
||||
<label className="block text-sm text-gray-400 mb-2 flex items-center gap-2">
|
||||
<GitMerge size={14} />
|
||||
Allowed Subagents
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 mb-2">Select which other agents this agent can spawn as subagents.</p>
|
||||
{agents.filter(a => a.id !== agentForm.id).length === 0 ? (
|
||||
<p className="text-xs text-gray-600 italic">No other agents available. Create more agents first.</p>
|
||||
) : (
|
||||
<div className="space-y-1.5 max-h-32 overflow-y-auto">
|
||||
{agents.filter(a => a.id !== agentForm.id).map(a => {
|
||||
const allowed = agentForm.subagents?.allow_agents || [];
|
||||
const isChecked = allowed.includes(a.id);
|
||||
return (
|
||||
<div key={a.id} className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`sub-${a.id}`}
|
||||
checked={isChecked}
|
||||
onChange={e => {
|
||||
const newList = e.target.checked
|
||||
? [...allowed, a.id]
|
||||
: allowed.filter(x => x !== a.id);
|
||||
setAgentForm({
|
||||
...agentForm,
|
||||
subagents: { allow_agents: newList.length > 0 ? newList : null }
|
||||
});
|
||||
}}
|
||||
className="w-4 h-4 rounded bg-dark-600 border-dark-500 text-claw-500 focus:ring-claw-500/50"
|
||||
/>
|
||||
<label htmlFor={`sub-${a.id}`} className="text-sm text-gray-300 select-none">{a.name || a.id}</label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-4 border-t border-dark-600 flex justify-end gap-3 flex-shrink-0">
|
||||
|
||||
Reference in New Issue
Block a user