mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-24 21:48:36 +02:00
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>
183 lines
6.1 KiB
TypeScript
183 lines
6.1 KiB
TypeScript
import { describe, expect, test } from 'bun:test';
|
|
|
|
import {
|
|
resolvePatternAgent,
|
|
resolvePatternAgents,
|
|
findWorkspaceAgentUsages,
|
|
normalizeWorkspaceAgentDefinition,
|
|
type WorkspaceAgentDefinition,
|
|
} from '@shared/domain/workspaceAgent';
|
|
import type { PatternAgentDefinition, PatternDefinition } from '@shared/domain/pattern';
|
|
|
|
const TIMESTAMP = '2026-04-01T00:00:00.000Z';
|
|
|
|
function makeWorkspaceAgent(overrides: Partial<WorkspaceAgentDefinition> = {}): WorkspaceAgentDefinition {
|
|
return {
|
|
id: 'wa-1',
|
|
name: 'Code Reviewer',
|
|
description: 'Reviews code for quality',
|
|
instructions: 'Review all code carefully',
|
|
model: 'gpt-5.4',
|
|
reasoningEffort: 'high',
|
|
createdAt: TIMESTAMP,
|
|
updatedAt: TIMESTAMP,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makeInlineAgent(overrides: Partial<PatternAgentDefinition> = {}): PatternAgentDefinition {
|
|
return {
|
|
id: 'agent-1',
|
|
name: 'Inline Agent',
|
|
description: 'An inline agent',
|
|
instructions: 'Do stuff',
|
|
model: 'claude-sonnet-4',
|
|
reasoningEffort: 'medium',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makeLinkedAgent(overrides: Partial<PatternAgentDefinition> = {}): PatternAgentDefinition {
|
|
return {
|
|
id: 'agent-linked',
|
|
name: 'Code Reviewer',
|
|
description: 'Reviews code for quality',
|
|
instructions: 'Review all code carefully',
|
|
model: 'gpt-5.4',
|
|
reasoningEffort: 'high',
|
|
workspaceAgentId: 'wa-1',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makePattern(agents: PatternAgentDefinition[], overrides: Partial<PatternDefinition> = {}): PatternDefinition {
|
|
return {
|
|
id: 'pattern-1',
|
|
name: 'Test Pattern',
|
|
description: '',
|
|
mode: 'sequential',
|
|
availability: 'available',
|
|
maxIterations: 10,
|
|
agents,
|
|
createdAt: TIMESTAMP,
|
|
updatedAt: TIMESTAMP,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('resolvePatternAgent', () => {
|
|
const workspaceAgents = [makeWorkspaceAgent()];
|
|
|
|
test('returns inline agent unchanged', () => {
|
|
const agent = makeInlineAgent();
|
|
const resolved = resolvePatternAgent(agent, workspaceAgents);
|
|
expect(resolved).toEqual(agent);
|
|
});
|
|
|
|
test('resolves linked agent from workspace agent base', () => {
|
|
const agent = makeLinkedAgent();
|
|
const resolved = resolvePatternAgent(agent, workspaceAgents);
|
|
expect(resolved.name).toBe('Code Reviewer');
|
|
expect(resolved.model).toBe('gpt-5.4');
|
|
expect(resolved.instructions).toBe('Review all code carefully');
|
|
expect(resolved.workspaceAgentId).toBe('wa-1');
|
|
expect(resolved.id).toBe('agent-linked');
|
|
});
|
|
|
|
test('applies per-pattern overrides on top of workspace agent', () => {
|
|
const agent = makeLinkedAgent({
|
|
overrides: { model: 'claude-opus-4', instructions: 'Override instructions' },
|
|
});
|
|
const resolved = resolvePatternAgent(agent, workspaceAgents);
|
|
expect(resolved.model).toBe('claude-opus-4');
|
|
expect(resolved.instructions).toBe('Override instructions');
|
|
expect(resolved.name).toBe('Code Reviewer');
|
|
expect(resolved.description).toBe('Reviews code for quality');
|
|
});
|
|
|
|
test('falls back to inline fields when workspace agent is missing', () => {
|
|
const agent = makeLinkedAgent({ workspaceAgentId: 'nonexistent' });
|
|
const resolved = resolvePatternAgent(agent, workspaceAgents);
|
|
expect(resolved).toEqual(agent);
|
|
});
|
|
|
|
test('partial overrides only replace specified fields', () => {
|
|
const agent = makeLinkedAgent({
|
|
overrides: { name: 'Custom Name' },
|
|
});
|
|
const resolved = resolvePatternAgent(agent, workspaceAgents);
|
|
expect(resolved.name).toBe('Custom Name');
|
|
expect(resolved.model).toBe('gpt-5.4');
|
|
expect(resolved.reasoningEffort).toBe('high');
|
|
});
|
|
});
|
|
|
|
describe('resolvePatternAgents', () => {
|
|
const workspaceAgents = [makeWorkspaceAgent()];
|
|
|
|
test('resolves all agents in a pattern', () => {
|
|
const pattern = makePattern([
|
|
makeInlineAgent(),
|
|
makeLinkedAgent(),
|
|
]);
|
|
const resolved = resolvePatternAgents(pattern, workspaceAgents);
|
|
expect(resolved.agents[0].name).toBe('Inline Agent');
|
|
expect(resolved.agents[1].name).toBe('Code Reviewer');
|
|
expect(resolved.agents[1].workspaceAgentId).toBe('wa-1');
|
|
});
|
|
|
|
test('preserves pattern metadata', () => {
|
|
const pattern = makePattern([makeInlineAgent()], { id: 'p-custom', name: 'Custom' });
|
|
const resolved = resolvePatternAgents(pattern, workspaceAgents);
|
|
expect(resolved.id).toBe('p-custom');
|
|
expect(resolved.name).toBe('Custom');
|
|
});
|
|
});
|
|
|
|
describe('findWorkspaceAgentUsages', () => {
|
|
test('finds patterns referencing a workspace agent', () => {
|
|
const patterns = [
|
|
makePattern([makeLinkedAgent()], { id: 'p1', name: 'Pattern 1' }),
|
|
makePattern([makeInlineAgent()], { id: 'p2', name: 'Pattern 2' }),
|
|
makePattern(
|
|
[makeInlineAgent(), makeLinkedAgent({ id: 'agent-linked-2' })],
|
|
{ id: 'p3', name: 'Pattern 3' },
|
|
),
|
|
];
|
|
const usages = findWorkspaceAgentUsages('wa-1', patterns);
|
|
expect(usages).toHaveLength(2);
|
|
expect(usages[0].patternId).toBe('p1');
|
|
expect(usages[1].patternId).toBe('p3');
|
|
});
|
|
|
|
test('returns empty when no patterns reference the agent', () => {
|
|
const patterns = [makePattern([makeInlineAgent()])];
|
|
const usages = findWorkspaceAgentUsages('wa-1', patterns);
|
|
expect(usages).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('normalizeWorkspaceAgentDefinition', () => {
|
|
test('trims string fields', () => {
|
|
const agent = makeWorkspaceAgent({
|
|
name: ' Code Reviewer ',
|
|
description: ' Reviews code ',
|
|
instructions: ' Review carefully ',
|
|
model: ' gpt-5.4 ',
|
|
});
|
|
const normalized = normalizeWorkspaceAgentDefinition(agent);
|
|
expect(normalized.name).toBe('Code Reviewer');
|
|
expect(normalized.description).toBe('Reviews code');
|
|
expect(normalized.instructions).toBe('Review carefully');
|
|
expect(normalized.model).toBe('gpt-5.4');
|
|
});
|
|
|
|
test('preserves non-string fields', () => {
|
|
const agent = makeWorkspaceAgent({ reasoningEffort: 'xhigh' });
|
|
const normalized = normalizeWorkspaceAgentDefinition(agent);
|
|
expect(normalized.reasoningEffort).toBe('xhigh');
|
|
expect(normalized.id).toBe('wa-1');
|
|
expect(normalized.createdAt).toBe(TIMESTAMP);
|
|
});
|
|
});
|