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:
MrFadiAi
2026-02-16 07:18:21 +01:00
parent b04f4d6f45
commit 5f8f6643cb
6 changed files with 221 additions and 5 deletions
+1 -1
View File
@@ -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 -1
View File
@@ -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"
+82 -1
View File
@@ -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> {
+1
View File
@@ -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 -1
View File
@@ -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",
+135 -1
View File
@@ -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">