feat: add workspace agents for reusable agent definitions across patterns

Introduce a workspace-level agent library that allows users to define
agents once and reference them from multiple orchestration patterns.

Domain model:
- Add WorkspaceAgentDefinition type with name, model, instructions, etc.
- Extend PatternAgentDefinition with optional workspaceAgentId and overrides
- Add resolution helpers that merge workspace agent base with per-pattern overrides
- Extend WorkspaceSettings with agents array and normalize on load

IPC & main process:
- Add saveWorkspaceAgent/deleteWorkspaceAgent IPC channels and handlers
- Resolve workspace agent references in buildEffectivePattern before
  sending to sidecar (no C# changes needed)

Settings UI:
- Add 'Agents' tab under Orchestration in the Settings panel
- Create WorkspaceAgentEditor component using ToolingEditorShell
- Show usage count (which patterns reference each agent)

Pattern editor integration:
- Add agent picker dropdown: 'New inline agent' or 'From library'
- Show linked badge (chain icon) on referenced agent graph nodes
- Show linked workspace agent banner in the inspector
- Add 'Save to Agent Library' action to promote inline agents
- Add 'Unlink' action to convert referenced agents back to inline

Tests:
- Add unit tests for resolution helpers (resolvePatternAgent,
  resolvePatternAgents, findWorkspaceAgentUsages, normalize)
- Update existing tooling test for new agents field

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-04-02 17:09:54 +02:00
co-authored by Copilot
parent 10316a2871
commit e39fffaf3b
17 changed files with 808 additions and 17 deletions
+45 -5
View File
@@ -36,6 +36,11 @@ import {
type ReasoningEffort,
validatePatternDefinition,
} from '@shared/domain/pattern';
import {
normalizeWorkspaceAgentDefinition,
resolvePatternAgents,
type WorkspaceAgentDefinition,
} from '@shared/domain/workspaceAgent';
import {
applyDiscoveredMcpServerStatus,
listAcceptedDiscoveredMcpServers,
@@ -813,6 +818,39 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
return this.persistAndBroadcast(workspace);
}
async saveWorkspaceAgent(agent: WorkspaceAgentDefinition): Promise<WorkspaceState> {
const workspace = await this.loadWorkspace();
const agents = workspace.settings.agents ?? [];
const existingIndex = agents.findIndex((current) => current.id === agent.id);
const timestamp = nowIso();
const candidate = normalizeWorkspaceAgentDefinition({
...agent,
createdAt: existingIndex >= 0 ? agents[existingIndex].createdAt : timestamp,
updatedAt: timestamp,
});
if (!candidate.name) {
throw new Error('Workspace agent name is required.');
}
if (existingIndex >= 0) {
agents[existingIndex] = candidate;
} else {
agents.push(candidate);
}
workspace.settings.agents = agents;
return this.persistAndBroadcast(workspace);
}
async deleteWorkspaceAgent(agentId: string): Promise<WorkspaceState> {
const workspace = await this.loadWorkspace();
workspace.settings.agents = (workspace.settings.agents ?? []).filter(
(agent) => agent.id !== agentId,
);
return this.persistAndBroadcast(workspace);
}
async createSession(projectId: string, patternId: string): Promise<WorkspaceState> {
const workspace = await this.loadWorkspace();
const project = this.requireProject(workspace, projectId);
@@ -955,7 +993,7 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
const project = this.requireProject(workspace, session.projectId);
const pattern = this.requirePattern(workspace, session.patternId);
const effectivePattern = this.applyProjectCustomizationToPattern(
await this.buildEffectivePattern(pattern, session),
await this.buildEffectivePattern(pattern, session, workspace.settings.agents ?? []),
project,
);
const projectInstructions = resolveProjectInstructionsContent(project.customization);
@@ -1014,7 +1052,7 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
const project = this.requireProject(workspace, session.projectId);
const pattern = this.requirePattern(workspace, session.patternId);
const effectivePattern = this.applyProjectCustomizationToPattern(
await this.buildEffectivePattern(pattern, session),
await this.buildEffectivePattern(pattern, session, workspace.settings.agents ?? []),
project,
);
const projectInstructions = resolveProjectInstructionsContent(project.customization);
@@ -1068,7 +1106,7 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
const project = this.requireProject(workspace, session.projectId);
const pattern = this.requirePattern(workspace, session.patternId);
const effectivePattern = this.applyProjectCustomizationToPattern(
await this.buildEffectivePattern(pattern, session),
await this.buildEffectivePattern(pattern, session, workspace.settings.agents ?? []),
project,
);
const projectInstructions = resolveProjectInstructionsContent(project.customization);
@@ -2919,10 +2957,12 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
private async buildEffectivePattern(
pattern: PatternDefinition,
session: SessionRecord,
workspaceAgents: ReadonlyArray<WorkspaceAgentDefinition>,
): Promise<PatternDefinition> {
const resolvedPattern = resolvePatternAgents(pattern, workspaceAgents);
const patternWithSessionConfig = session.sessionModelConfig
? applySessionModelConfig(pattern, session)
: pattern;
? applySessionModelConfig(resolvedPattern, session)
: resolvedPattern;
const patternWithApprovalSettings = applySessionApprovalSettings(patternWithSessionConfig, session);
const modelCatalog = await this.loadAvailableModelCatalog();
+7
View File
@@ -34,6 +34,7 @@ import type {
SaveLspProfileInput,
SaveMcpServerInput,
SavePatternInput,
SaveWorkspaceAgentInput,
SendSessionMessageInput,
SetPatternFavoriteInput,
SetProjectAgentProfileEnabledInput,
@@ -148,6 +149,12 @@ export function registerIpcHandlers(
ipcMain.handle(ipcChannels.deleteLspProfile, (_event, profileId: string) =>
service.deleteLspProfile(profileId),
);
ipcMain.handle(ipcChannels.saveWorkspaceAgent, (_event, input: SaveWorkspaceAgentInput) =>
service.saveWorkspaceAgent(input.agent),
);
ipcMain.handle(ipcChannels.deleteWorkspaceAgent, (_event, agentId: string) =>
service.deleteWorkspaceAgent(agentId),
);
ipcMain.handle(ipcChannels.describeTerminal, () => service.describeTerminal());
ipcMain.handle(ipcChannels.createTerminal, () => service.createTerminal());
ipcMain.handle(ipcChannels.restartTerminal, () => service.restartTerminal());