Skip to content

Commit a6a4fa9

Browse files
committed
feat: OpenClaw multi-agent workspace export (#42)
2 parents fe1ff02 + cc60a4c commit a6a4fa9

File tree

2 files changed

+154
-23
lines changed

2 files changed

+154
-23
lines changed

src/adapters/openclaw.ts

Lines changed: 110 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,21 @@ import { loadAllSkills, getAllowedTools } from '../utils/skill-loader.js';
1515
* - TOOLS.md (tool definitions)
1616
* - skills/<name>/SKILL.md (skill files, passed through)
1717
*/
18+
export interface SubAgentExport {
19+
name: string;
20+
soulMd: string;
21+
agentsMd: string;
22+
toolsMd: string;
23+
skills: Array<{ name: string; content: string }>;
24+
}
25+
1826
export interface OpenClawExport {
1927
config: object;
2028
agentsMd: string;
2129
soulMd: string;
2230
toolsMd: string;
2331
skills: Array<{ name: string; content: string }>;
32+
subAgents: SubAgentExport[];
2433
}
2534

2635
export function exportToOpenClaw(dir: string): OpenClawExport {
@@ -42,7 +51,10 @@ export function exportToOpenClaw(dir: string): OpenClawExport {
4251
// --- Skills (passthrough SKILL.md files) ---
4352
const skills = collectSkills(agentDir);
4453

45-
return { config, agentsMd, soulMd, toolsMd, skills };
54+
// --- Sub-agents (separate workspaces) ---
55+
const subAgents = exportSubAgents(agentDir, manifest);
56+
57+
return { config, agentsMd, soulMd, toolsMd, skills, subAgents };
4658
}
4759

4860
/**
@@ -52,40 +64,99 @@ export function exportToOpenClaw(dir: string): OpenClawExport {
5264
export function exportToOpenClawString(dir: string): string {
5365
const exp = exportToOpenClaw(dir);
5466
const parts: string[] = [];
67+
const hasSubAgents = exp.subAgents.length > 0;
68+
const mainPrefix = hasSubAgents ? `workspace-${exp.config && (exp.config as Record<string, Record<string, string[]>>).agents?.list?.[0] || 'main'}` : 'workspace';
5569

5670
parts.push('# === openclaw.json ===');
5771
parts.push(JSON.stringify(exp.config, null, 2));
5872

59-
parts.push('\n# === workspace/AGENTS.md ===');
73+
parts.push(`\n# === ${mainPrefix}/AGENTS.md ===`);
6074
parts.push(exp.agentsMd);
6175

62-
parts.push('\n# === workspace/SOUL.md ===');
76+
parts.push(`\n# === ${mainPrefix}/SOUL.md ===`);
6377
parts.push(exp.soulMd);
6478

6579
if (exp.toolsMd) {
66-
parts.push('\n# === workspace/TOOLS.md ===');
80+
parts.push(`\n# === ${mainPrefix}/TOOLS.md ===`);
6781
parts.push(exp.toolsMd);
6882
}
6983

7084
for (const skill of exp.skills) {
71-
parts.push(`\n# === workspace/skills/${skill.name}/SKILL.md ===`);
85+
parts.push(`\n# === ${mainPrefix}/skills/${skill.name}/SKILL.md ===`);
7286
parts.push(skill.content);
7387
}
7488

89+
// Sub-agent workspaces
90+
for (const sub of exp.subAgents) {
91+
const prefix = `workspace-${sub.name}`;
92+
93+
parts.push(`\n# === ${prefix}/SOUL.md ===`);
94+
parts.push(sub.soulMd);
95+
96+
parts.push(`\n# === ${prefix}/AGENTS.md ===`);
97+
parts.push(sub.agentsMd);
98+
99+
if (sub.toolsMd) {
100+
parts.push(`\n# === ${prefix}/TOOLS.md ===`);
101+
parts.push(sub.toolsMd);
102+
}
103+
104+
for (const skill of sub.skills) {
105+
parts.push(`\n# === ${prefix}/skills/${skill.name}/SKILL.md ===`);
106+
parts.push(skill.content);
107+
}
108+
}
109+
75110
return parts.join('\n');
76111
}
77112

78113
function buildOpenClawConfig(agentDir: string, manifest: ReturnType<typeof loadAgentManifest>): object {
114+
const mainModel = mapModelName(manifest.model?.preferred ?? 'anthropic/claude-sonnet-4-5-20250929');
115+
116+
// Check for sub-agents → multi-agent config
117+
if (manifest.agents && Object.keys(manifest.agents).length > 0) {
118+
const agentNames = ['main', ...Object.keys(manifest.agents)];
119+
const agents: Record<string, unknown> = {
120+
list: agentNames,
121+
main: buildAgentConfig(mainModel, `~/.openclaw/workspace-${manifest.name}`, manifest),
122+
};
123+
124+
for (const name of Object.keys(manifest.agents)) {
125+
const subDir = join(agentDir, 'agents', name);
126+
let subModel = mainModel;
127+
if (existsSync(join(subDir, 'agent.yaml'))) {
128+
try {
129+
const subManifest = loadAgentManifest(subDir);
130+
if (subManifest.model?.preferred) {
131+
subModel = mapModelName(subManifest.model.preferred);
132+
}
133+
} catch { /* use parent model */ }
134+
}
135+
agents[name] = {
136+
model: subModel,
137+
workspace: `~/.openclaw/workspace-${name}`,
138+
};
139+
}
140+
141+
return { agents };
142+
}
143+
144+
// Single-agent config (unchanged)
79145
const config: Record<string, unknown> = {
80-
agent: {
81-
model: mapModelName(manifest.model?.preferred ?? 'anthropic/claude-sonnet-4-5-20250929'),
82-
workspace: '~/.openclaw/workspace',
83-
},
146+
agent: buildAgentConfig(mainModel, '~/.openclaw/workspace', manifest),
84147
};
85148

86-
// Map runtime settings
149+
return config;
150+
}
151+
152+
function buildAgentConfig(
153+
model: string,
154+
workspace: string,
155+
manifest: ReturnType<typeof loadAgentManifest>,
156+
): Record<string, unknown> {
157+
const agentConfig: Record<string, unknown> = { model, workspace };
158+
87159
if (manifest.runtime) {
88-
const agentConfig = config.agent as Record<string, unknown>;
89160
if (manifest.runtime.temperature !== undefined) {
90161
agentConfig.temperature = manifest.runtime.temperature;
91162
}
@@ -94,12 +165,11 @@ function buildOpenClawConfig(agentDir: string, manifest: ReturnType<typeof loadA
94165
}
95166
}
96167

97-
// Map model constraints
98168
if (manifest.model?.constraints?.max_tokens) {
99-
(config.agent as Record<string, unknown>).maxTokens = manifest.model.constraints.max_tokens;
169+
agentConfig.maxTokens = manifest.model.constraints.max_tokens;
100170
}
101171

102-
return config;
172+
return agentConfig;
103173
}
104174

105175
/**
@@ -246,6 +316,32 @@ function buildToolsMd(agentDir: string): string {
246316
return parts.join('\n');
247317
}
248318

319+
function exportSubAgents(
320+
agentDir: string,
321+
manifest: ReturnType<typeof loadAgentManifest>,
322+
): SubAgentExport[] {
323+
if (!manifest.agents) return [];
324+
325+
const subAgents: SubAgentExport[] = [];
326+
327+
for (const name of Object.keys(manifest.agents)) {
328+
const subDir = join(agentDir, 'agents', name);
329+
if (!existsSync(subDir)) continue;
330+
331+
try {
332+
const subManifest = loadAgentManifest(subDir);
333+
const soulMd = loadFileIfExists(join(subDir, 'SOUL.md')) ?? `# ${subManifest.name}\n${subManifest.description}`;
334+
const agentsMd = buildAgentsMd(subDir, subManifest);
335+
const toolsMd = buildToolsMd(subDir);
336+
const skills = collectSkills(subDir);
337+
338+
subAgents.push({ name, soulMd, agentsMd, toolsMd, skills });
339+
} catch { /* skip malformed sub-agents */ }
340+
}
341+
342+
return subAgents;
343+
}
344+
249345
function collectSkills(agentDir: string): Array<{ name: string; content: string }> {
250346
const skills: Array<{ name: string; content: string }> = [];
251347
const skillsDir = join(agentDir, 'skills');

src/runners/openclaw.ts

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,60 @@ export function runWithOpenClaw(agentDir: string, manifest: AgentManifest, optio
2020
const workspaceDir = join(tmpdir(), `gitagent-openclaw-${randomBytes(4).toString('hex')}`);
2121
mkdirSync(workspaceDir, { recursive: true });
2222

23-
// Write workspace files
24-
writeFileSync(join(workspaceDir, 'AGENTS.md'), exp.agentsMd, 'utf-8');
25-
writeFileSync(join(workspaceDir, 'SOUL.md'), exp.soulMd, 'utf-8');
23+
const hasSubAgents = exp.subAgents.length > 0;
24+
25+
// Write main workspace files
26+
const mainWorkspace = hasSubAgents ? join(workspaceDir, `workspace-main`) : workspaceDir;
27+
mkdirSync(mainWorkspace, { recursive: true });
28+
29+
writeFileSync(join(mainWorkspace, 'AGENTS.md'), exp.agentsMd, 'utf-8');
30+
writeFileSync(join(mainWorkspace, 'SOUL.md'), exp.soulMd, 'utf-8');
2631

2732
if (exp.toolsMd) {
28-
writeFileSync(join(workspaceDir, 'TOOLS.md'), exp.toolsMd, 'utf-8');
33+
writeFileSync(join(mainWorkspace, 'TOOLS.md'), exp.toolsMd, 'utf-8');
2934
}
3035

31-
// Write skills
36+
// Write main skills
3237
for (const skill of exp.skills) {
33-
const skillDir = join(workspaceDir, 'skills', skill.name);
38+
const skillDir = join(mainWorkspace, 'skills', skill.name);
3439
mkdirSync(skillDir, { recursive: true });
3540
writeFileSync(join(skillDir, 'SKILL.md'), skill.content, 'utf-8');
3641
}
3742

38-
// Write openclaw.json config, pointing workspace to our temp dir
43+
// Write sub-agent workspaces
44+
for (const sub of exp.subAgents) {
45+
const subWorkspace = join(workspaceDir, `workspace-${sub.name}`);
46+
mkdirSync(subWorkspace, { recursive: true });
47+
48+
writeFileSync(join(subWorkspace, 'SOUL.md'), sub.soulMd, 'utf-8');
49+
writeFileSync(join(subWorkspace, 'AGENTS.md'), sub.agentsMd, 'utf-8');
50+
if (sub.toolsMd) {
51+
writeFileSync(join(subWorkspace, 'TOOLS.md'), sub.toolsMd, 'utf-8');
52+
}
53+
for (const skill of sub.skills) {
54+
const skillDir = join(subWorkspace, 'skills', skill.name);
55+
mkdirSync(skillDir, { recursive: true });
56+
writeFileSync(join(skillDir, 'SKILL.md'), skill.content, 'utf-8');
57+
}
58+
info(` Sub-agent workspace: workspace-${sub.name}/`);
59+
}
60+
61+
// Write openclaw.json config, pointing workspaces to temp dirs
3962
const config = exp.config as Record<string, Record<string, unknown>>;
40-
config.agent = config.agent ?? {};
41-
config.agent.workspace = workspaceDir;
63+
if (hasSubAgents) {
64+
const agents = config.agents as Record<string, unknown>;
65+
if (agents.main && typeof agents.main === 'object') {
66+
(agents.main as Record<string, unknown>).workspace = mainWorkspace;
67+
}
68+
for (const sub of exp.subAgents) {
69+
if (agents[sub.name] && typeof agents[sub.name] === 'object') {
70+
(agents[sub.name] as Record<string, unknown>).workspace = join(workspaceDir, `workspace-${sub.name}`);
71+
}
72+
}
73+
} else {
74+
config.agent = config.agent ?? {};
75+
config.agent.workspace = workspaceDir;
76+
}
4277

4378
const configFile = join(workspaceDir, 'openclaw.json');
4479
writeFileSync(configFile, JSON.stringify(config, null, 2), 'utf-8');

0 commit comments

Comments
 (0)