feat(Agnet): support multi agents (#385)
This commit is contained in:
107
electron/api/routes/agents.ts
Normal file
107
electron/api/routes/agents.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import type { IncomingMessage, ServerResponse } from 'http';
|
||||
import {
|
||||
assignChannelToAgent,
|
||||
clearChannelBinding,
|
||||
createAgent,
|
||||
deleteAgentConfig,
|
||||
listAgentsSnapshot,
|
||||
updateAgentName,
|
||||
} from '../../utils/agent-config';
|
||||
import { deleteChannelConfig } from '../../utils/channel-config';
|
||||
import type { HostApiContext } from '../context';
|
||||
import { parseJsonBody, sendJson } from '../route-utils';
|
||||
|
||||
function scheduleGatewayReload(ctx: HostApiContext, reason: string): void {
|
||||
if (ctx.gatewayManager.getStatus().state !== 'stopped') {
|
||||
ctx.gatewayManager.debouncedReload();
|
||||
return;
|
||||
}
|
||||
void reason;
|
||||
}
|
||||
|
||||
export async function handleAgentRoutes(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
url: URL,
|
||||
ctx: HostApiContext,
|
||||
): Promise<boolean> {
|
||||
if (url.pathname === '/api/agents' && req.method === 'GET') {
|
||||
sendJson(res, 200, { success: true, ...(await listAgentsSnapshot()) });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/agents' && req.method === 'POST') {
|
||||
try {
|
||||
const body = await parseJsonBody<{ name: string }>(req);
|
||||
const snapshot = await createAgent(body.name);
|
||||
scheduleGatewayReload(ctx, 'create-agent');
|
||||
sendJson(res, 200, { success: true, ...snapshot });
|
||||
} catch (error) {
|
||||
sendJson(res, 500, { success: false, error: String(error) });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith('/api/agents/') && req.method === 'PUT') {
|
||||
const suffix = url.pathname.slice('/api/agents/'.length);
|
||||
const parts = suffix.split('/').filter(Boolean);
|
||||
|
||||
if (parts.length === 1) {
|
||||
try {
|
||||
const body = await parseJsonBody<{ name: string }>(req);
|
||||
const agentId = decodeURIComponent(parts[0]);
|
||||
const snapshot = await updateAgentName(agentId, body.name);
|
||||
scheduleGatewayReload(ctx, 'update-agent');
|
||||
sendJson(res, 200, { success: true, ...snapshot });
|
||||
} catch (error) {
|
||||
sendJson(res, 500, { success: false, error: String(error) });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (parts.length === 3 && parts[1] === 'channels') {
|
||||
try {
|
||||
const agentId = decodeURIComponent(parts[0]);
|
||||
const channelType = decodeURIComponent(parts[2]);
|
||||
const snapshot = await assignChannelToAgent(agentId, channelType);
|
||||
scheduleGatewayReload(ctx, 'assign-channel');
|
||||
sendJson(res, 200, { success: true, ...snapshot });
|
||||
} catch (error) {
|
||||
sendJson(res, 500, { success: false, error: String(error) });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith('/api/agents/') && req.method === 'DELETE') {
|
||||
const suffix = url.pathname.slice('/api/agents/'.length);
|
||||
const parts = suffix.split('/').filter(Boolean);
|
||||
|
||||
if (parts.length === 1) {
|
||||
try {
|
||||
const agentId = decodeURIComponent(parts[0]);
|
||||
const snapshot = await deleteAgentConfig(agentId);
|
||||
scheduleGatewayReload(ctx, 'delete-agent');
|
||||
sendJson(res, 200, { success: true, ...snapshot });
|
||||
} catch (error) {
|
||||
sendJson(res, 500, { success: false, error: String(error) });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (parts.length === 3 && parts[1] === 'channels') {
|
||||
try {
|
||||
const channelType = decodeURIComponent(parts[2]);
|
||||
await deleteChannelConfig(channelType);
|
||||
const snapshot = await clearChannelBinding(channelType);
|
||||
scheduleGatewayReload(ctx, 'remove-agent-channel');
|
||||
sendJson(res, 200, { success: true, ...snapshot });
|
||||
} catch (error) {
|
||||
sendJson(res, 500, { success: false, error: String(error) });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { handleAppRoutes } from './routes/app';
|
||||
import { handleGatewayRoutes } from './routes/gateway';
|
||||
import { handleSettingsRoutes } from './routes/settings';
|
||||
import { handleProviderRoutes } from './routes/providers';
|
||||
import { handleAgentRoutes } from './routes/agents';
|
||||
import { handleChannelRoutes } from './routes/channels';
|
||||
import { handleLogRoutes } from './routes/logs';
|
||||
import { handleUsageRoutes } from './routes/usage';
|
||||
@@ -27,6 +28,7 @@ const routeHandlers: RouteHandler[] = [
|
||||
handleGatewayRoutes,
|
||||
handleSettingsRoutes,
|
||||
handleProviderRoutes,
|
||||
handleAgentRoutes,
|
||||
handleChannelRoutes,
|
||||
handleSkillRoutes,
|
||||
handleFileRoutes,
|
||||
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
} from '../services/providers/provider-runtime-sync';
|
||||
import { validateApiKeyWithProvider } from '../services/providers/provider-validation';
|
||||
import { appUpdater } from './updater';
|
||||
import { PORTS } from '../utils/config';
|
||||
|
||||
type AppRequest = {
|
||||
id?: string;
|
||||
@@ -141,8 +142,66 @@ export function registerIpcHandlers(
|
||||
registerFileHandlers();
|
||||
}
|
||||
|
||||
type HostApiFetchRequest = {
|
||||
path: string;
|
||||
method?: string;
|
||||
headers?: Record<string, string>;
|
||||
body?: unknown;
|
||||
};
|
||||
|
||||
function registerHostApiProxyHandlers(): void {
|
||||
// Host API proxy handlers - currently disabled
|
||||
ipcMain.handle('hostapi:fetch', async (_, request: HostApiFetchRequest) => {
|
||||
try {
|
||||
const path = typeof request?.path === 'string' ? request.path : '';
|
||||
if (!path || !path.startsWith('/')) {
|
||||
throw new Error(`Invalid host API path: ${String(request?.path)}`);
|
||||
}
|
||||
|
||||
const method = (request.method || 'GET').toUpperCase();
|
||||
const headers: Record<string, string> = { ...(request.headers || {}) };
|
||||
let body: string | undefined;
|
||||
|
||||
if (request.body !== undefined && request.body !== null) {
|
||||
if (typeof request.body === 'string') {
|
||||
body = request.body;
|
||||
} else {
|
||||
body = JSON.stringify(request.body);
|
||||
if (!headers['Content-Type'] && !headers['content-type']) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const response = await proxyAwareFetch(`http://127.0.0.1:${PORTS.CLAWX_HOST_API}${path}`, {
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
|
||||
const data: { status: number; ok: boolean; json?: unknown; text?: string } = {
|
||||
status: response.status,
|
||||
ok: response.ok,
|
||||
};
|
||||
|
||||
if (response.status !== 204) {
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
if (contentType.includes('application/json')) {
|
||||
data.json = await response.json().catch(() => undefined);
|
||||
} else {
|
||||
data.text = await response.text().catch(() => '');
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: true, data };
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function mapAppErrorCode(error: unknown): AppErrorCode {
|
||||
|
||||
452
electron/utils/agent-config.ts
Normal file
452
electron/utils/agent-config.ts
Normal file
@@ -0,0 +1,452 @@
|
||||
import { access, copyFile, mkdir, readdir } from 'fs/promises';
|
||||
import { constants } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { listConfiguredChannels, readOpenClawConfig, writeOpenClawConfig } from './channel-config';
|
||||
import { expandPath, getOpenClawConfigDir } from './paths';
|
||||
import * as logger from './logger';
|
||||
|
||||
const MAIN_AGENT_ID = 'main';
|
||||
const MAIN_AGENT_NAME = 'Main';
|
||||
const DEFAULT_WORKSPACE_PATH = '~/.openclaw/workspace';
|
||||
const AGENT_BOOTSTRAP_FILES = [
|
||||
'AGENTS.md',
|
||||
'SOUL.md',
|
||||
'TOOLS.md',
|
||||
'USER.md',
|
||||
'IDENTITY.md',
|
||||
'HEARTBEAT.md',
|
||||
'BOOT.md',
|
||||
];
|
||||
const AGENT_RUNTIME_FILES = [
|
||||
'auth-profiles.json',
|
||||
'models.json',
|
||||
];
|
||||
|
||||
interface AgentModelConfig {
|
||||
primary?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface AgentDefaultsConfig {
|
||||
workspace?: string;
|
||||
model?: string | AgentModelConfig;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface AgentListEntry extends Record<string, unknown> {
|
||||
id: string;
|
||||
name?: string;
|
||||
default?: boolean;
|
||||
workspace?: string;
|
||||
agentDir?: string;
|
||||
model?: string | AgentModelConfig;
|
||||
}
|
||||
|
||||
interface AgentsConfig extends Record<string, unknown> {
|
||||
defaults?: AgentDefaultsConfig;
|
||||
list?: AgentListEntry[];
|
||||
}
|
||||
|
||||
interface BindingMatch extends Record<string, unknown> {
|
||||
channel?: string;
|
||||
}
|
||||
|
||||
interface BindingConfig extends Record<string, unknown> {
|
||||
agentId?: string;
|
||||
match?: BindingMatch;
|
||||
}
|
||||
|
||||
interface AgentConfigDocument extends Record<string, unknown> {
|
||||
agents?: AgentsConfig;
|
||||
bindings?: BindingConfig[];
|
||||
}
|
||||
|
||||
export interface AgentSummary {
|
||||
id: string;
|
||||
name: string;
|
||||
isDefault: boolean;
|
||||
modelDisplay: string;
|
||||
inheritedModel: boolean;
|
||||
workspace: string;
|
||||
agentDir: string;
|
||||
channelTypes: string[];
|
||||
}
|
||||
|
||||
export interface AgentsSnapshot {
|
||||
agents: AgentSummary[];
|
||||
defaultAgentId: string;
|
||||
configuredChannelTypes: string[];
|
||||
channelOwners: Record<string, string>;
|
||||
}
|
||||
|
||||
function formatModelLabel(model: unknown): string | null {
|
||||
if (typeof model === 'string' && model.trim()) {
|
||||
const trimmed = model.trim();
|
||||
const parts = trimmed.split('/');
|
||||
return parts[parts.length - 1] || trimmed;
|
||||
}
|
||||
|
||||
if (model && typeof model === 'object') {
|
||||
const primary = (model as AgentModelConfig).primary;
|
||||
if (typeof primary === 'string' && primary.trim()) {
|
||||
const parts = primary.trim().split('/');
|
||||
return parts[parts.length - 1] || primary.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeAgentName(name: string): string {
|
||||
return name.trim() || 'Agent';
|
||||
}
|
||||
|
||||
function slugifyAgentId(name: string): string {
|
||||
const normalized = name
|
||||
.normalize('NFKD')
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.toLowerCase()
|
||||
.replace(/[_\s]+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
|
||||
if (!normalized) return 'agent';
|
||||
if (normalized === MAIN_AGENT_ID) return 'agent';
|
||||
return normalized;
|
||||
}
|
||||
|
||||
async function fileExists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await access(path, constants.F_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureDir(path: string): Promise<void> {
|
||||
if (!(await fileExists(path))) {
|
||||
await mkdir(path, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultWorkspacePath(config: AgentConfigDocument): string {
|
||||
const defaults = (config.agents && typeof config.agents === 'object'
|
||||
? (config.agents as AgentsConfig).defaults
|
||||
: undefined);
|
||||
return typeof defaults?.workspace === 'string' && defaults.workspace.trim()
|
||||
? defaults.workspace
|
||||
: DEFAULT_WORKSPACE_PATH;
|
||||
}
|
||||
|
||||
function getDefaultAgentDirPath(agentId: string): string {
|
||||
return `~/.openclaw/agents/${agentId}/agent`;
|
||||
}
|
||||
|
||||
function createImplicitMainEntry(config: AgentConfigDocument): AgentListEntry {
|
||||
return {
|
||||
id: MAIN_AGENT_ID,
|
||||
name: MAIN_AGENT_NAME,
|
||||
default: true,
|
||||
workspace: getDefaultWorkspacePath(config),
|
||||
agentDir: getDefaultAgentDirPath(MAIN_AGENT_ID),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeAgentsConfig(config: AgentConfigDocument): {
|
||||
agentsConfig: AgentsConfig;
|
||||
entries: AgentListEntry[];
|
||||
defaultAgentId: string;
|
||||
syntheticMain: boolean;
|
||||
} {
|
||||
const agentsConfig = (config.agents && typeof config.agents === 'object'
|
||||
? { ...(config.agents as AgentsConfig) }
|
||||
: {}) as AgentsConfig;
|
||||
const rawEntries = Array.isArray(agentsConfig.list)
|
||||
? agentsConfig.list.filter((entry): entry is AgentListEntry => (
|
||||
Boolean(entry) && typeof entry === 'object' && typeof entry.id === 'string' && entry.id.trim().length > 0
|
||||
))
|
||||
: [];
|
||||
|
||||
if (rawEntries.length === 0) {
|
||||
const main = createImplicitMainEntry(config);
|
||||
return {
|
||||
agentsConfig,
|
||||
entries: [main],
|
||||
defaultAgentId: MAIN_AGENT_ID,
|
||||
syntheticMain: true,
|
||||
};
|
||||
}
|
||||
|
||||
const defaultEntry = rawEntries.find((entry) => entry.default) ?? rawEntries[0];
|
||||
return {
|
||||
agentsConfig,
|
||||
entries: rawEntries.map((entry) => ({ ...entry })),
|
||||
defaultAgentId: defaultEntry.id,
|
||||
syntheticMain: false,
|
||||
};
|
||||
}
|
||||
|
||||
function isSimpleChannelBinding(binding: unknown): binding is BindingConfig {
|
||||
if (!binding || typeof binding !== 'object') return false;
|
||||
const candidate = binding as BindingConfig;
|
||||
if (typeof candidate.agentId !== 'string' || !candidate.agentId) return false;
|
||||
if (!candidate.match || typeof candidate.match !== 'object' || Array.isArray(candidate.match)) return false;
|
||||
const keys = Object.keys(candidate.match);
|
||||
return keys.length === 1 && typeof candidate.match.channel === 'string' && Boolean(candidate.match.channel);
|
||||
}
|
||||
|
||||
/** Normalize agent ID for consistent comparison (bindings vs entries). */
|
||||
function normalizeAgentIdForBinding(id: string): string {
|
||||
return (id ?? '').trim().toLowerCase() || '';
|
||||
}
|
||||
|
||||
function getSimpleChannelBindingMap(bindings: unknown): Map<string, string> {
|
||||
const owners = new Map<string, string>();
|
||||
if (!Array.isArray(bindings)) return owners;
|
||||
|
||||
for (const binding of bindings) {
|
||||
if (!isSimpleChannelBinding(binding)) continue;
|
||||
const agentId = normalizeAgentIdForBinding(binding.agentId!);
|
||||
if (agentId) owners.set(binding.match.channel!, agentId);
|
||||
}
|
||||
|
||||
return owners;
|
||||
}
|
||||
|
||||
function upsertBindingsForChannel(
|
||||
bindings: unknown,
|
||||
channelType: string,
|
||||
agentId: string | null,
|
||||
): BindingConfig[] | undefined {
|
||||
const nextBindings = Array.isArray(bindings)
|
||||
? [...bindings as BindingConfig[]].filter((binding) => !(
|
||||
isSimpleChannelBinding(binding) && binding.match.channel === channelType
|
||||
))
|
||||
: [];
|
||||
|
||||
if (agentId) {
|
||||
nextBindings.push({
|
||||
agentId,
|
||||
match: { channel: channelType },
|
||||
});
|
||||
}
|
||||
|
||||
return nextBindings.length > 0 ? nextBindings : undefined;
|
||||
}
|
||||
|
||||
async function listExistingAgentIdsOnDisk(): Promise<Set<string>> {
|
||||
const ids = new Set<string>();
|
||||
const agentsDir = join(getOpenClawConfigDir(), 'agents');
|
||||
|
||||
try {
|
||||
if (!(await fileExists(agentsDir))) return ids;
|
||||
const entries = await readdir(agentsDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) ids.add(entry.name);
|
||||
}
|
||||
} catch {
|
||||
// ignore discovery failures
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
async function copyBootstrapFiles(sourceWorkspace: string, targetWorkspace: string): Promise<void> {
|
||||
await ensureDir(targetWorkspace);
|
||||
|
||||
for (const fileName of AGENT_BOOTSTRAP_FILES) {
|
||||
const source = join(sourceWorkspace, fileName);
|
||||
const target = join(targetWorkspace, fileName);
|
||||
if (!(await fileExists(source)) || (await fileExists(target))) continue;
|
||||
await copyFile(source, target);
|
||||
}
|
||||
}
|
||||
|
||||
async function copyRuntimeFiles(sourceAgentDir: string, targetAgentDir: string): Promise<void> {
|
||||
await ensureDir(targetAgentDir);
|
||||
|
||||
for (const fileName of AGENT_RUNTIME_FILES) {
|
||||
const source = join(sourceAgentDir, fileName);
|
||||
const target = join(targetAgentDir, fileName);
|
||||
if (!(await fileExists(source)) || (await fileExists(target))) continue;
|
||||
await copyFile(source, target);
|
||||
}
|
||||
}
|
||||
|
||||
async function provisionAgentFilesystem(config: AgentConfigDocument, agent: AgentListEntry): Promise<void> {
|
||||
const { entries } = normalizeAgentsConfig(config);
|
||||
const mainEntry = entries.find((entry) => entry.id === MAIN_AGENT_ID) ?? createImplicitMainEntry(config);
|
||||
const sourceWorkspace = expandPath(mainEntry.workspace || getDefaultWorkspacePath(config));
|
||||
const targetWorkspace = expandPath(agent.workspace || `~/.openclaw/workspace-${agent.id}`);
|
||||
const sourceAgentDir = expandPath(mainEntry.agentDir || getDefaultAgentDirPath(MAIN_AGENT_ID));
|
||||
const targetAgentDir = expandPath(agent.agentDir || getDefaultAgentDirPath(agent.id));
|
||||
const targetSessionsDir = join(getOpenClawConfigDir(), 'agents', agent.id, 'sessions');
|
||||
|
||||
await ensureDir(targetWorkspace);
|
||||
await ensureDir(targetAgentDir);
|
||||
await ensureDir(targetSessionsDir);
|
||||
|
||||
if (targetWorkspace !== sourceWorkspace) {
|
||||
await copyBootstrapFiles(sourceWorkspace, targetWorkspace);
|
||||
}
|
||||
if (targetAgentDir !== sourceAgentDir) {
|
||||
await copyRuntimeFiles(sourceAgentDir, targetAgentDir);
|
||||
}
|
||||
}
|
||||
|
||||
async function buildSnapshotFromConfig(config: AgentConfigDocument): Promise<AgentsSnapshot> {
|
||||
const { entries, defaultAgentId } = normalizeAgentsConfig(config);
|
||||
const configuredChannels = await listConfiguredChannels();
|
||||
const explicitOwners = getSimpleChannelBindingMap(config.bindings);
|
||||
const defaultAgentIdNorm = normalizeAgentIdForBinding(defaultAgentId);
|
||||
const channelOwners: Record<string, string> = {};
|
||||
|
||||
for (const channelType of configuredChannels) {
|
||||
channelOwners[channelType] = explicitOwners.get(channelType) || defaultAgentIdNorm;
|
||||
}
|
||||
|
||||
const defaultModelLabel = formatModelLabel((config.agents as AgentsConfig | undefined)?.defaults?.model);
|
||||
const agents: AgentSummary[] = entries.map((entry) => {
|
||||
const modelLabel = formatModelLabel(entry.model) || defaultModelLabel || 'Not configured';
|
||||
const inheritedModel = !formatModelLabel(entry.model) && Boolean(defaultModelLabel);
|
||||
const entryIdNorm = normalizeAgentIdForBinding(entry.id);
|
||||
return {
|
||||
id: entry.id,
|
||||
name: entry.name || (entry.id === MAIN_AGENT_ID ? MAIN_AGENT_NAME : entry.id),
|
||||
isDefault: entry.id === defaultAgentId,
|
||||
modelDisplay: modelLabel,
|
||||
inheritedModel,
|
||||
workspace: entry.workspace || (entry.id === MAIN_AGENT_ID ? getDefaultWorkspacePath(config) : `~/.openclaw/workspace-${entry.id}`),
|
||||
agentDir: entry.agentDir || getDefaultAgentDirPath(entry.id),
|
||||
channelTypes: configuredChannels.filter((channelType) => channelOwners[channelType] === entryIdNorm),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
agents,
|
||||
defaultAgentId,
|
||||
configuredChannelTypes: configuredChannels,
|
||||
channelOwners,
|
||||
};
|
||||
}
|
||||
|
||||
export async function listAgentsSnapshot(): Promise<AgentsSnapshot> {
|
||||
const config = await readOpenClawConfig() as AgentConfigDocument;
|
||||
return buildSnapshotFromConfig(config);
|
||||
}
|
||||
|
||||
export async function createAgent(name: string): Promise<AgentsSnapshot> {
|
||||
const config = await readOpenClawConfig() as AgentConfigDocument;
|
||||
const { agentsConfig, entries, syntheticMain } = normalizeAgentsConfig(config);
|
||||
const normalizedName = normalizeAgentName(name);
|
||||
const existingIds = new Set(entries.map((entry) => entry.id));
|
||||
const diskIds = await listExistingAgentIdsOnDisk();
|
||||
let nextId = slugifyAgentId(normalizedName);
|
||||
let suffix = 2;
|
||||
|
||||
while (existingIds.has(nextId) || diskIds.has(nextId)) {
|
||||
nextId = `${slugifyAgentId(normalizedName)}-${suffix}`;
|
||||
suffix += 1;
|
||||
}
|
||||
|
||||
const nextEntries = syntheticMain ? [createImplicitMainEntry(config), ...entries.filter((entry, index) => index > 0)] : [...entries];
|
||||
const newAgent: AgentListEntry = {
|
||||
id: nextId,
|
||||
name: normalizedName,
|
||||
workspace: `~/.openclaw/workspace-${nextId}`,
|
||||
agentDir: getDefaultAgentDirPath(nextId),
|
||||
};
|
||||
|
||||
if (!nextEntries.some((entry) => entry.id === MAIN_AGENT_ID) && syntheticMain) {
|
||||
nextEntries.unshift(createImplicitMainEntry(config));
|
||||
}
|
||||
nextEntries.push(newAgent);
|
||||
|
||||
config.agents = {
|
||||
...agentsConfig,
|
||||
list: nextEntries,
|
||||
};
|
||||
|
||||
await provisionAgentFilesystem(config, newAgent);
|
||||
await writeOpenClawConfig(config);
|
||||
logger.info('Created agent config entry', { agentId: nextId });
|
||||
return buildSnapshotFromConfig(config);
|
||||
}
|
||||
|
||||
export async function updateAgentName(agentId: string, name: string): Promise<AgentsSnapshot> {
|
||||
const config = await readOpenClawConfig() as AgentConfigDocument;
|
||||
const { agentsConfig, entries } = normalizeAgentsConfig(config);
|
||||
const normalizedName = normalizeAgentName(name);
|
||||
const index = entries.findIndex((entry) => entry.id === agentId);
|
||||
if (index === -1) {
|
||||
throw new Error(`Agent "${agentId}" not found`);
|
||||
}
|
||||
|
||||
entries[index] = {
|
||||
...entries[index],
|
||||
name: normalizedName,
|
||||
};
|
||||
|
||||
config.agents = {
|
||||
...agentsConfig,
|
||||
list: entries,
|
||||
};
|
||||
|
||||
await writeOpenClawConfig(config);
|
||||
logger.info('Updated agent name', { agentId, name: normalizedName });
|
||||
return buildSnapshotFromConfig(config);
|
||||
}
|
||||
|
||||
export async function deleteAgentConfig(agentId: string): Promise<AgentsSnapshot> {
|
||||
if (agentId === MAIN_AGENT_ID) {
|
||||
throw new Error('The main agent cannot be deleted');
|
||||
}
|
||||
|
||||
const config = await readOpenClawConfig() as AgentConfigDocument;
|
||||
const { agentsConfig, entries, defaultAgentId } = normalizeAgentsConfig(config);
|
||||
const nextEntries = entries.filter((entry) => entry.id !== agentId);
|
||||
if (nextEntries.length === entries.length) {
|
||||
throw new Error(`Agent "${agentId}" not found`);
|
||||
}
|
||||
|
||||
config.agents = {
|
||||
...agentsConfig,
|
||||
list: nextEntries,
|
||||
};
|
||||
config.bindings = Array.isArray(config.bindings)
|
||||
? config.bindings.filter((binding) => !(isSimpleChannelBinding(binding) && binding.agentId === agentId))
|
||||
: undefined;
|
||||
|
||||
if (defaultAgentId === agentId && nextEntries.length > 0) {
|
||||
nextEntries[0] = {
|
||||
...nextEntries[0],
|
||||
default: true,
|
||||
};
|
||||
}
|
||||
|
||||
await writeOpenClawConfig(config);
|
||||
logger.info('Deleted agent config entry', { agentId });
|
||||
return buildSnapshotFromConfig(config);
|
||||
}
|
||||
|
||||
export async function assignChannelToAgent(agentId: string, channelType: string): Promise<AgentsSnapshot> {
|
||||
const config = await readOpenClawConfig() as AgentConfigDocument;
|
||||
const { entries } = normalizeAgentsConfig(config);
|
||||
if (!entries.some((entry) => entry.id === agentId)) {
|
||||
throw new Error(`Agent "${agentId}" not found`);
|
||||
}
|
||||
|
||||
config.bindings = upsertBindingsForChannel(config.bindings, channelType, agentId);
|
||||
await writeOpenClawConfig(config);
|
||||
logger.info('Assigned channel to agent', { agentId, channelType });
|
||||
return buildSnapshotFromConfig(config);
|
||||
}
|
||||
|
||||
export async function clearChannelBinding(channelType: string): Promise<AgentsSnapshot> {
|
||||
const config = await readOpenClawConfig() as AgentConfigDocument;
|
||||
config.bindings = upsertBindingsForChannel(config.bindings, channelType, null);
|
||||
await writeOpenClawConfig(config);
|
||||
logger.info('Cleared simplified channel binding', { channelType });
|
||||
return buildSnapshotFromConfig(config);
|
||||
}
|
||||
Reference in New Issue
Block a user