mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
feat(agent): expose agent library surface (#3383)
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { AgentDefinitionDisplay } from '../../services/api/agentLibraryApi';
|
||||
import AgentsLibraryPanel from './AgentsLibraryPanel';
|
||||
|
||||
const mockListDefinitions = vi.fn();
|
||||
|
||||
vi.mock('../../services/api/agentLibraryApi', () => ({
|
||||
agentLibraryApi: { listDefinitions: (...args: unknown[]) => mockListDefinitions(...args) },
|
||||
}));
|
||||
|
||||
function agent(overrides: Partial<AgentDefinitionDisplay> = {}): AgentDefinitionDisplay {
|
||||
return {
|
||||
id: 'researcher',
|
||||
display_name: 'Researcher',
|
||||
when_to_use: 'Use for focused research.',
|
||||
tier: 'worker',
|
||||
model: { kind: 'hint', value: 'reasoning' },
|
||||
direct_tool_count: 1,
|
||||
direct_tool_names: ['web_search'],
|
||||
uses_wildcard_tools: false,
|
||||
subagent_ids: [],
|
||||
includes_profile: false,
|
||||
includes_memory_md: false,
|
||||
includes_memory_context: false,
|
||||
can_run_as_user_facing_worker: true,
|
||||
write_capable: false,
|
||||
source: 'builtin',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('AgentsLibraryPanel', () => {
|
||||
beforeEach(() => {
|
||||
mockListDefinitions.mockReset();
|
||||
Object.assign(navigator, { clipboard: { writeText: vi.fn().mockResolvedValue(undefined) } });
|
||||
});
|
||||
|
||||
it('shows loading then an empty state', async () => {
|
||||
mockListDefinitions.mockResolvedValueOnce([]);
|
||||
render(<AgentsLibraryPanel onRunAgentTask={vi.fn()} />);
|
||||
|
||||
expect(screen.getByText(/loading agents/i)).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/no runnable agents/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows a load error', async () => {
|
||||
mockListDefinitions.mockRejectedValueOnce(new Error('registry unavailable'));
|
||||
render(<AgentsLibraryPanel onRunAgentTask={vi.fn()} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/registry unavailable/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders safe metadata and filters non-runnable chat agents', async () => {
|
||||
mockListDefinitions.mockResolvedValueOnce([
|
||||
agent({ model: { kind: 'inherit' } }),
|
||||
agent({
|
||||
id: 'orchestrator',
|
||||
display_name: 'Orchestrator',
|
||||
tier: 'chat',
|
||||
can_run_as_user_facing_worker: false,
|
||||
}),
|
||||
]);
|
||||
render(<AgentsLibraryPanel onRunAgentTask={vi.fn()} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Researcher')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByText('Orchestrator')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('Inherit')).toBeInTheDocument();
|
||||
expect(screen.getByText('Read-only')).toBeInTheDocument();
|
||||
expect(screen.getByText('1 tool')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('runs a one-off task for the selected agent', async () => {
|
||||
mockListDefinitions.mockResolvedValueOnce([agent()]);
|
||||
const onRun = vi.fn().mockResolvedValue(undefined);
|
||||
render(<AgentsLibraryPanel onRunAgentTask={onRun} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Researcher')).toBeInTheDocument();
|
||||
});
|
||||
fireEvent.change(screen.getByPlaceholderText(/task for this agent/i), {
|
||||
target: { value: 'Find current docs' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: /run task/i }));
|
||||
|
||||
await waitFor(() => expect(onRun).toHaveBeenCalledTimes(1));
|
||||
expect(onRun).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: 'researcher' }),
|
||||
'Find current docs'
|
||||
);
|
||||
});
|
||||
|
||||
it('copies an agent id', async () => {
|
||||
mockListDefinitions.mockResolvedValueOnce([agent()]);
|
||||
render(<AgentsLibraryPanel onRunAgentTask={vi.fn()} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Researcher')).toBeInTheDocument();
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: /copy id/i }));
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('researcher');
|
||||
});
|
||||
|
||||
it('does not show copied when clipboard write is unavailable', async () => {
|
||||
mockListDefinitions.mockResolvedValueOnce([agent()]);
|
||||
Object.assign(navigator, { clipboard: {} });
|
||||
render(<AgentsLibraryPanel onRunAgentTask={vi.fn()} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Researcher')).toBeInTheDocument();
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: /copy id/i }));
|
||||
expect(screen.queryByRole('button', { name: /copied/i })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,238 @@
|
||||
import debug from 'debug';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { LuBot, LuClipboard, LuPlay, LuRefreshCw, LuSend } from 'react-icons/lu';
|
||||
|
||||
import { useT } from '../../lib/i18n/I18nContext';
|
||||
import { type AgentDefinitionDisplay, agentLibraryApi } from '../../services/api/agentLibraryApi';
|
||||
|
||||
const log = debug('intelligence:agents-library');
|
||||
|
||||
interface AgentsLibraryPanelProps {
|
||||
onRunAgentTask: (agent: AgentDefinitionDisplay, prompt: string) => Promise<void>;
|
||||
runningAgentId?: string | null;
|
||||
}
|
||||
|
||||
function modelLabel(
|
||||
agent: AgentDefinitionDisplay,
|
||||
t: (key: string, fallback?: string) => string
|
||||
): string {
|
||||
if (agent.model.kind === 'inherit') return t('intelligence.agents.model.inherit', 'inherit');
|
||||
return agent.model.value ? `${agent.model.kind}:${agent.model.value}` : agent.model.kind;
|
||||
}
|
||||
|
||||
function capabilityChips(
|
||||
agent: AgentDefinitionDisplay,
|
||||
t: (key: string, fallback?: string) => string
|
||||
) {
|
||||
const chips = [t(`intelligence.agents.tier.${agent.tier}`, agent.tier)];
|
||||
chips.push(
|
||||
agent.write_capable ? t('intelligence.agents.writeCapable') : t('intelligence.agents.readOnly')
|
||||
);
|
||||
if (agent.uses_wildcard_tools) {
|
||||
chips.push(t('intelligence.agents.allTools'));
|
||||
} else if (agent.direct_tool_count > 0) {
|
||||
chips.push(
|
||||
(agent.direct_tool_count === 1
|
||||
? t('intelligence.agents.toolCountOne')
|
||||
: t('intelligence.agents.toolCountOther')
|
||||
).replace('{count}', String(agent.direct_tool_count))
|
||||
);
|
||||
}
|
||||
if (agent.subagent_ids.length > 0) {
|
||||
chips.push(
|
||||
(agent.subagent_ids.length === 1
|
||||
? t('intelligence.agents.subagentCountOne')
|
||||
: t('intelligence.agents.subagentCountOther')
|
||||
).replace('{count}', String(agent.subagent_ids.length))
|
||||
);
|
||||
}
|
||||
return chips;
|
||||
}
|
||||
|
||||
export default function AgentsLibraryPanel({
|
||||
onRunAgentTask,
|
||||
runningAgentId,
|
||||
}: AgentsLibraryPanelProps) {
|
||||
const { t } = useT();
|
||||
const [agents, setAgents] = useState<AgentDefinitionDisplay[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [drafts, setDrafts] = useState<Record<string, string>>({});
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||
|
||||
const loadAgents = useCallback(async () => {
|
||||
log('[ui-flow][agents-library] load entry');
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const definitions = await agentLibraryApi.listDefinitions();
|
||||
setAgents(definitions);
|
||||
log('[ui-flow][agents-library] load exit count=%d', definitions.length);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
log('[ui-flow][agents-library] load error=%s', msg);
|
||||
setError(msg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handle = window.setTimeout(() => {
|
||||
void loadAgents();
|
||||
}, 0);
|
||||
return () => window.clearTimeout(handle);
|
||||
}, [loadAgents]);
|
||||
|
||||
const visibleAgents = useMemo(
|
||||
() => agents.filter(agent => agent.can_run_as_user_facing_worker),
|
||||
[agents]
|
||||
);
|
||||
|
||||
const handleCopy = useCallback(async (id: string) => {
|
||||
try {
|
||||
if (!navigator.clipboard?.writeText) return;
|
||||
await navigator.clipboard.writeText(id);
|
||||
setCopiedId(id);
|
||||
window.setTimeout(() => setCopiedId(current => (current === id ? null : current)), 1600);
|
||||
} catch (err) {
|
||||
log('[ui-flow][agents-library] copy failed id=%s error=%o', id, err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleRun = useCallback(
|
||||
async (agent: AgentDefinitionDisplay, prompt: string) => {
|
||||
const trimmed = prompt.trim();
|
||||
if (!trimmed || runningAgentId) return;
|
||||
log('[ui-flow][agents-library] run entry agent=%s chars=%d', agent.id, trimmed.length);
|
||||
await onRunAgentTask(agent, trimmed);
|
||||
setDrafts(prev => ({ ...prev, [agent.id]: '' }));
|
||||
log('[ui-flow][agents-library] run exit agent=%s', agent.id);
|
||||
},
|
||||
[onRunAgentTask, runningAgentId]
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="space-y-3">
|
||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<h3 className="flex items-center gap-2 truncate text-sm font-semibold text-stone-700 dark:text-neutral-200">
|
||||
<LuBot className="h-4 w-4 text-ocean-500" />
|
||||
{t('intelligence.agents.title')}
|
||||
</h3>
|
||||
<p className="mt-1 text-xs text-stone-400 dark:text-neutral-500">
|
||||
{t('intelligence.agents.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void loadAgents()}
|
||||
className="inline-flex items-center gap-1.5 rounded-md border border-stone-200 px-2.5 py-1.5 text-xs font-medium text-stone-600 hover:bg-stone-50 dark:border-neutral-800 dark:text-neutral-300 dark:hover:bg-neutral-800">
|
||||
<LuRefreshCw className={`h-3.5 w-3.5 ${loading ? 'animate-spin' : ''}`} />
|
||||
{t('intelligence.agents.refresh')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading && (
|
||||
<div className="flex items-center justify-center rounded-xl border border-stone-200 py-5 text-sm text-stone-400 dark:border-neutral-800 dark:text-neutral-500">
|
||||
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-ocean-500 border-t-transparent" />
|
||||
{t('intelligence.agents.loading')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && !loading && (
|
||||
<div className="rounded-xl border border-coral-200 bg-coral-50 px-4 py-3 text-sm text-coral-700 dark:border-coral-500/30 dark:bg-coral-500/10 dark:text-coral-300">
|
||||
{t('intelligence.agents.failedToLoad')}: {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && visibleAgents.length === 0 && (
|
||||
<div className="rounded-xl border border-dashed border-stone-200 py-6 text-center text-sm text-stone-400 dark:border-neutral-800 dark:text-neutral-500">
|
||||
{t('intelligence.agents.empty')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && visibleAgents.length > 0 && (
|
||||
<ul className="divide-y divide-stone-100 rounded-xl border border-stone-200 bg-white dark:divide-neutral-800 dark:border-neutral-800 dark:bg-neutral-900">
|
||||
{visibleAgents.map(agent => {
|
||||
const draft = drafts[agent.id] ?? '';
|
||||
const running = runningAgentId === agent.id;
|
||||
return (
|
||||
<li key={agent.id} className="space-y-3 p-3">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="min-w-0 space-y-1">
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<span className="truncate text-sm font-semibold text-stone-800 dark:text-neutral-100">
|
||||
{agent.display_name}
|
||||
</span>
|
||||
<span className="rounded-md bg-stone-100 px-1.5 py-0.5 font-mono text-[10px] text-stone-500 dark:bg-neutral-800 dark:text-neutral-400">
|
||||
{agent.id}
|
||||
</span>
|
||||
<span className="rounded-md bg-ocean-50 px-1.5 py-0.5 text-[10px] font-medium text-ocean-700 dark:bg-ocean-500/10 dark:text-ocean-200">
|
||||
{modelLabel(agent, t)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs leading-5 text-stone-500 dark:text-neutral-400">
|
||||
{agent.when_to_use}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{capabilityChips(agent, t).map(chip => (
|
||||
<span
|
||||
key={chip}
|
||||
className="rounded-md bg-stone-50 px-1.5 py-0.5 text-[10px] font-medium text-stone-500 dark:bg-neutral-800 dark:text-neutral-400">
|
||||
{chip}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-none flex-wrap gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
void handleRun(agent, t('intelligence.agents.startChatPrompt'))
|
||||
}
|
||||
disabled={Boolean(runningAgentId)}
|
||||
className="inline-flex items-center gap-1.5 rounded-md border border-stone-200 px-2.5 py-1.5 text-xs font-medium text-stone-600 hover:bg-stone-50 disabled:opacity-50 dark:border-neutral-800 dark:text-neutral-300 dark:hover:bg-neutral-800">
|
||||
<LuSend className="h-3.5 w-3.5" />
|
||||
{t('intelligence.agents.startChat')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleCopy(agent.id)}
|
||||
className="inline-flex items-center gap-1.5 rounded-md border border-stone-200 px-2.5 py-1.5 text-xs font-medium text-stone-600 hover:bg-stone-50 dark:border-neutral-800 dark:text-neutral-300 dark:hover:bg-neutral-800">
|
||||
<LuClipboard className="h-3.5 w-3.5" />
|
||||
{copiedId === agent.id
|
||||
? t('intelligence.agents.copied')
|
||||
: t('intelligence.agents.copyId')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<input
|
||||
type="text"
|
||||
aria-label={t('intelligence.agents.taskPlaceholder')}
|
||||
value={draft}
|
||||
onChange={event =>
|
||||
setDrafts(prev => ({ ...prev, [agent.id]: event.target.value }))
|
||||
}
|
||||
placeholder={t('intelligence.agents.taskPlaceholder')}
|
||||
className="min-w-0 flex-1 rounded-md border border-stone-200 bg-white px-3 py-2 text-sm text-stone-800 placeholder:text-stone-400 focus:border-ocean-400 focus:outline-none focus:ring-2 focus:ring-ocean-100 dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-100 dark:placeholder:text-neutral-600"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleRun(agent, draft)}
|
||||
disabled={!draft.trim() || Boolean(runningAgentId)}
|
||||
className="inline-flex flex-none items-center justify-center gap-1.5 rounded-md bg-ocean-600 px-3 py-2 text-sm font-medium text-white hover:bg-ocean-700 disabled:opacity-50">
|
||||
<LuPlay className="h-4 w-4" />
|
||||
{running ? t('intelligence.agents.running') : t('intelligence.agents.runTask')}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useT } from '../../lib/i18n/I18nContext';
|
||||
import { TaskKanbanBoard } from '../../pages/conversations/components/TaskKanbanBoard';
|
||||
import type { AgentDefinitionDisplay } from '../../services/api/agentLibraryApi';
|
||||
import { threadApi } from '../../services/api/threadApi';
|
||||
import {
|
||||
TASK_SOURCES_THREAD_ID,
|
||||
@@ -46,6 +47,7 @@ import {
|
||||
} from '../../store/threadSlice';
|
||||
import type { ThreadMessage } from '../../types/thread';
|
||||
import type { TaskBoard, TaskBoardCard, TaskBoardCardStatus } from '../../types/turnState';
|
||||
import AgentsLibraryPanel from './AgentsLibraryPanel';
|
||||
import { UserTaskComposer } from './UserTaskComposer';
|
||||
|
||||
const log = debug('intelligence:tasks');
|
||||
@@ -114,6 +116,16 @@ function buildAgentTaskPrompt(
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function buildExplicitAgentPrompt(agent: AgentDefinitionDisplay, task: string): string {
|
||||
return [
|
||||
`@agent:${agent.id}`,
|
||||
'',
|
||||
'Run this task with the explicitly selected agent above. Treat the selected agent id as the user routing choice when resolving delegation ambiguity.',
|
||||
'',
|
||||
task.trim(),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export default function IntelligenceTasksTab() {
|
||||
const { t } = useT();
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -129,6 +141,7 @@ export default function IntelligenceTasksTab() {
|
||||
const [composerOpen, setComposerOpen] = useState(false);
|
||||
const [refiningCard, setRefiningCard] = useState<TaskBoardCard | null>(null);
|
||||
const [workingCardId, setWorkingCardId] = useState<string | null>(null);
|
||||
const [runningAgentId, setRunningAgentId] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [actionError, setActionError] = useState<string | null>(null);
|
||||
@@ -394,6 +407,53 @@ export default function IntelligenceTasksTab() {
|
||||
[dispatch, navigate, personalBoard, selectedAgentProfileId, t, uiLocale, workingCardId]
|
||||
);
|
||||
|
||||
const handleRunAgentTask = useCallback(
|
||||
async (agent: AgentDefinitionDisplay, task: string) => {
|
||||
if (runningAgentId) return;
|
||||
setRunningAgentId(agent.id);
|
||||
setActionError(null);
|
||||
const now = new Date().toISOString();
|
||||
const launchPrompt = buildExplicitAgentPrompt(agent, task);
|
||||
const titleBase = task.trim().slice(0, 64) || agent.display_name;
|
||||
try {
|
||||
const thread = await threadApi.createNewThread([AGENT_TASK_THREAD_LABEL, 'agent-library']);
|
||||
await threadApi.updateTitle(thread.id, agentTaskThreadTitle(titleBase));
|
||||
const userMessage: ThreadMessage = {
|
||||
id: `msg_${globalThis.crypto?.randomUUID ? globalThis.crypto.randomUUID() : `${Date.now()}`}`,
|
||||
content: launchPrompt,
|
||||
type: 'text',
|
||||
extraMetadata: { source: 'agent-library', explicitAgentId: agent.id },
|
||||
sender: 'user',
|
||||
createdAt: now,
|
||||
};
|
||||
await threadApi.appendMessage(thread.id, userMessage);
|
||||
|
||||
dispatch(setSelectedThread(thread.id));
|
||||
dispatch(setToolTimelineForThread({ threadId: thread.id, entries: [] }));
|
||||
dispatch(beginInferenceTurn({ threadId: thread.id }));
|
||||
dispatch(setActiveThread(thread.id));
|
||||
void dispatch(loadThreads());
|
||||
void dispatch(loadThreadMessages(thread.id));
|
||||
navigate('/chat');
|
||||
|
||||
await chatSend({
|
||||
threadId: thread.id,
|
||||
message: launchPrompt,
|
||||
model: CHAT_MODEL_ID,
|
||||
profileId: selectedAgentProfileId,
|
||||
locale: uiLocale,
|
||||
});
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
log('run explicit agent task failed agent=%s: %s', agent.id, msg);
|
||||
if (mountedRef.current) setActionError(t('intelligence.agents.runFailed'));
|
||||
} finally {
|
||||
if (mountedRef.current) setRunningAgentId(null);
|
||||
}
|
||||
},
|
||||
[dispatch, navigate, runningAgentId, selectedAgentProfileId, t, uiLocale]
|
||||
);
|
||||
|
||||
const handleApproveSourcePlan = useCallback(
|
||||
async (sourceCard: TaskBoardCard, draft: RefinedTaskDraft) => {
|
||||
const now = new Date().toISOString();
|
||||
@@ -498,6 +558,8 @@ export default function IntelligenceTasksTab() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AgentsLibraryPanel onRunAgentTask={handleRunAgentTask} runningAgentId={runningAgentId} />
|
||||
|
||||
{/* Personal board — always present so users can manage their own tasks. */}
|
||||
<section className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -20,6 +20,7 @@ const hoisted = vi.hoisted(() => ({
|
||||
updateTitle: vi.fn(),
|
||||
appendMessage: vi.fn(),
|
||||
chatSend: vi.fn(),
|
||||
listAgentDefinitions: vi.fn(),
|
||||
todosList: vi.fn(),
|
||||
todosAdd: vi.fn(),
|
||||
todosEdit: vi.fn(),
|
||||
@@ -44,6 +45,10 @@ vi.mock('../../../services/api/threadApi', () => ({
|
||||
|
||||
vi.mock('../../../services/chatService', () => ({ chatSend: hoisted.chatSend }));
|
||||
|
||||
vi.mock('../../../services/api/agentLibraryApi', () => ({
|
||||
agentLibraryApi: { listDefinitions: hoisted.listAgentDefinitions },
|
||||
}));
|
||||
|
||||
vi.mock('../../../services/api/todosApi', () => ({
|
||||
TASK_SOURCES_THREAD_ID: 'task-sources',
|
||||
USER_TASKS_THREAD_ID: 'user-tasks',
|
||||
@@ -168,6 +173,7 @@ describe('IntelligenceTasksTab', () => {
|
||||
hoisted.updateTitle.mockReset();
|
||||
hoisted.appendMessage.mockReset();
|
||||
hoisted.chatSend.mockReset();
|
||||
hoisted.listAgentDefinitions.mockReset();
|
||||
hoisted.todosList.mockReset();
|
||||
hoisted.todosAdd.mockReset();
|
||||
hoisted.todosEdit.mockReset();
|
||||
@@ -208,6 +214,25 @@ describe('IntelligenceTasksTab', () => {
|
||||
createdAt: '2026-01-01T00:00:00Z',
|
||||
});
|
||||
hoisted.chatSend.mockResolvedValue(undefined);
|
||||
hoisted.listAgentDefinitions.mockResolvedValue([
|
||||
{
|
||||
id: 'researcher',
|
||||
display_name: 'Researcher',
|
||||
when_to_use: 'Use for focused research.',
|
||||
tier: 'worker',
|
||||
model: { kind: 'hint', value: 'reasoning' },
|
||||
direct_tool_count: 1,
|
||||
direct_tool_names: ['web_search'],
|
||||
uses_wildcard_tools: false,
|
||||
subagent_ids: [],
|
||||
includes_profile: false,
|
||||
includes_memory_md: false,
|
||||
includes_memory_context: false,
|
||||
can_run_as_user_facing_worker: true,
|
||||
write_capable: false,
|
||||
source: 'builtin',
|
||||
},
|
||||
]);
|
||||
hoisted.todosList.mockImplementation((threadId: string) =>
|
||||
Promise.resolve(makeBoard(threadId, []))
|
||||
);
|
||||
@@ -477,6 +502,44 @@ describe('IntelligenceTasksTab', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('starts a labeled task thread from an explicit library agent selection', async () => {
|
||||
hoisted.todosUpdateStatus.mockResolvedValue(makeBoard('user-tasks', []));
|
||||
|
||||
vi.resetModules();
|
||||
const Tab = await importTab();
|
||||
renderTab(Tab);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Researcher')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText(/task for this agent/i), {
|
||||
target: { value: 'Find the current API docs' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: /Run task/i }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(hoisted.createNewThread).toHaveBeenCalledWith(['tasks', 'agent-library'])
|
||||
);
|
||||
expect(hoisted.appendMessage).toHaveBeenCalledWith(
|
||||
'thread-agent-task',
|
||||
expect.objectContaining({
|
||||
content: expect.stringContaining('@agent:researcher'),
|
||||
extraMetadata: expect.objectContaining({
|
||||
source: 'agent-library',
|
||||
explicitAgentId: 'researcher',
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect(hoisted.chatSend).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
threadId: 'thread-agent-task',
|
||||
message: expect.stringContaining('Find the current API docs'),
|
||||
model: 'reasoning-v1',
|
||||
profileId: 'agent-profile-1',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('deletes a personal card via the todos RPC', async () => {
|
||||
hoisted.todosList.mockImplementation((threadId: string) =>
|
||||
Promise.resolve(
|
||||
|
||||
@@ -4478,6 +4478,33 @@ const messages: TranslationMap = {
|
||||
'autocomplete.maxChars': 'أقصى عدد لأحرف السياق',
|
||||
'autocomplete.overlayTtlMs': 'مهلة الطبقة (مللي ثانية)',
|
||||
'memory.tab.council': 'Council',
|
||||
'intelligence.agents.title': 'مكتبة الوكلاء',
|
||||
'intelligence.agents.subtitle':
|
||||
'استعرض المتخصصين القابلين للتشغيل وأرسل مهمة واحدة إلى وكيل محدد.',
|
||||
'intelligence.agents.refresh': 'تحديث',
|
||||
'intelligence.agents.loading': 'جارٍ تحميل الوكلاء…',
|
||||
'intelligence.agents.failedToLoad': 'تعذر تحميل الوكلاء',
|
||||
'intelligence.agents.empty': 'لا توجد وكلاء قابلة للتشغيل.',
|
||||
'intelligence.agents.readOnly': 'قراءة فقط',
|
||||
'intelligence.agents.writeCapable': 'يمكنه الكتابة',
|
||||
'intelligence.agents.allTools': 'كل الأدوات',
|
||||
'intelligence.agents.toolCountOne': '{count} أداة',
|
||||
'intelligence.agents.toolCountOther': '{count} أدوات',
|
||||
'intelligence.agents.subagentCountOne': '{count} وكيل فرعي',
|
||||
'intelligence.agents.subagentCountOther': '{count} وكلاء فرعيين',
|
||||
'intelligence.agents.startChat': 'بدء الدردشة',
|
||||
'intelligence.agents.startChatPrompt':
|
||||
'ابدأ دردشة مع هذا الوكيل. عرّف بتخصصك، واسأل عمّا تحتاج إلى معرفته، ثم انتظر مهمتي.',
|
||||
'intelligence.agents.copyId': 'نسخ المعرّف',
|
||||
'intelligence.agents.copied': 'تم النسخ',
|
||||
'intelligence.agents.taskPlaceholder': 'مهمة لهذا الوكيل',
|
||||
'intelligence.agents.runTask': 'تشغيل المهمة',
|
||||
'intelligence.agents.running': 'قيد التشغيل…',
|
||||
'intelligence.agents.runFailed': 'تعذر بدء الوكيل المحدد',
|
||||
'intelligence.agents.model.inherit': 'وراثة',
|
||||
'intelligence.agents.tier.chat': 'دردشة',
|
||||
'intelligence.agents.tier.reasoning': 'استدلال',
|
||||
'intelligence.agents.tier.worker': 'عامل',
|
||||
'modelCouncil.title': 'Model Council',
|
||||
'modelCouncil.intro':
|
||||
'Ask one question, get independent answers from several models in parallel, then a chair model synthesizes where they agree, where they disagree, and what each uniquely adds.',
|
||||
|
||||
@@ -4562,6 +4562,33 @@ const messages: TranslationMap = {
|
||||
'autocomplete.maxChars': 'সর্বোচ্চ প্রসঙ্গ অক্ষর',
|
||||
'autocomplete.overlayTtlMs': 'ওভারলে টাইমআউট (মিলিসেকেন্ড)',
|
||||
'memory.tab.council': 'Council',
|
||||
'intelligence.agents.title': 'এজেন্ট লাইব্রেরি',
|
||||
'intelligence.agents.subtitle':
|
||||
'চালানো যায় এমন বিশেষজ্ঞদের দেখুন এবং নির্দিষ্ট এজেন্টকে একটি কাজ পাঠান।',
|
||||
'intelligence.agents.refresh': 'রিফ্রেশ',
|
||||
'intelligence.agents.loading': 'এজেন্ট লোড হচ্ছে…',
|
||||
'intelligence.agents.failedToLoad': 'এজেন্ট লোড করা যায়নি',
|
||||
'intelligence.agents.empty': 'চালানোর মতো কোনো এজেন্ট নেই।',
|
||||
'intelligence.agents.readOnly': 'শুধু পড়া',
|
||||
'intelligence.agents.writeCapable': 'লিখতে পারে',
|
||||
'intelligence.agents.allTools': 'সব টুল',
|
||||
'intelligence.agents.toolCountOne': '{count} টুল',
|
||||
'intelligence.agents.toolCountOther': '{count} টুল',
|
||||
'intelligence.agents.subagentCountOne': '{count} সাব-এজেন্ট',
|
||||
'intelligence.agents.subagentCountOther': '{count} সাব-এজেন্ট',
|
||||
'intelligence.agents.startChat': 'চ্যাট শুরু করুন',
|
||||
'intelligence.agents.startChatPrompt':
|
||||
'এই এজেন্টের সঙ্গে চ্যাট শুরু করুন। নিজের বিশেষত্ব জানান, কী জানা দরকার জিজ্ঞেস করুন, তারপর আমার কাজের অপেক্ষা করুন।',
|
||||
'intelligence.agents.copyId': 'ID কপি করুন',
|
||||
'intelligence.agents.copied': 'কপি হয়েছে',
|
||||
'intelligence.agents.taskPlaceholder': 'এই এজেন্টের কাজ',
|
||||
'intelligence.agents.runTask': 'কাজ চালান',
|
||||
'intelligence.agents.running': 'চলছে…',
|
||||
'intelligence.agents.runFailed': 'নির্বাচিত এজেন্ট শুরু করা যায়নি',
|
||||
'intelligence.agents.model.inherit': 'উত্তরাধিকার',
|
||||
'intelligence.agents.tier.chat': 'চ্যাট',
|
||||
'intelligence.agents.tier.reasoning': 'যুক্তি',
|
||||
'intelligence.agents.tier.worker': 'ওয়ার্কার',
|
||||
'modelCouncil.title': 'Model Council',
|
||||
'modelCouncil.intro':
|
||||
'Ask one question, get independent answers from several models in parallel, then a chair model synthesizes where they agree, where they disagree, and what each uniquely adds.',
|
||||
|
||||
@@ -4684,6 +4684,33 @@ const messages: TranslationMap = {
|
||||
'autocomplete.maxChars': 'Maximale Kontextzeichen',
|
||||
'autocomplete.overlayTtlMs': 'Overlay-Timeout (ms)',
|
||||
'memory.tab.council': 'Council',
|
||||
'intelligence.agents.title': 'Agentenbibliothek',
|
||||
'intelligence.agents.subtitle':
|
||||
'Prüfe ausführbare Spezialisten und sende eine Aufgabe an einen benannten Agenten.',
|
||||
'intelligence.agents.refresh': 'Aktualisieren',
|
||||
'intelligence.agents.loading': 'Agenten werden geladen…',
|
||||
'intelligence.agents.failedToLoad': 'Agenten konnten nicht geladen werden',
|
||||
'intelligence.agents.empty': 'Keine ausführbaren Agenten verfügbar.',
|
||||
'intelligence.agents.readOnly': 'Nur lesen',
|
||||
'intelligence.agents.writeCapable': 'Schreibfähig',
|
||||
'intelligence.agents.allTools': 'Alle Tools',
|
||||
'intelligence.agents.toolCountOne': '{count} Tool',
|
||||
'intelligence.agents.toolCountOther': '{count} Tools',
|
||||
'intelligence.agents.subagentCountOne': '{count} Subagent',
|
||||
'intelligence.agents.subagentCountOther': '{count} Subagenten',
|
||||
'intelligence.agents.startChat': 'Chat starten',
|
||||
'intelligence.agents.startChatPrompt':
|
||||
'Starte einen Chat mit diesem Agenten. Stelle deine Spezialisierung vor, frage nach dem benötigten Kontext und warte auf meine Aufgabe.',
|
||||
'intelligence.agents.copyId': 'ID kopieren',
|
||||
'intelligence.agents.copied': 'Kopiert',
|
||||
'intelligence.agents.taskPlaceholder': 'Aufgabe für diesen Agenten',
|
||||
'intelligence.agents.runTask': 'Aufgabe ausführen',
|
||||
'intelligence.agents.running': 'Läuft…',
|
||||
'intelligence.agents.runFailed': 'Der ausgewählte Agent konnte nicht gestartet werden',
|
||||
'intelligence.agents.model.inherit': 'Übernehmen',
|
||||
'intelligence.agents.tier.chat': 'Chat',
|
||||
'intelligence.agents.tier.reasoning': 'Reasoning',
|
||||
'intelligence.agents.tier.worker': 'Worker',
|
||||
'modelCouncil.title': 'Model Council',
|
||||
'modelCouncil.intro':
|
||||
'Ask one question, get independent answers from several models in parallel, then a chair model synthesizes where they agree, where they disagree, and what each uniquely adds.',
|
||||
|
||||
@@ -315,6 +315,33 @@ const en: TranslationMap = {
|
||||
'memory.tab.cohesion': 'Cohesion',
|
||||
'memory.tab.settings': 'Settings',
|
||||
'memory.tab.council': 'Council',
|
||||
'intelligence.agents.title': 'Agents Library',
|
||||
'intelligence.agents.subtitle':
|
||||
'Inspect runnable specialists and send one task to a named agent.',
|
||||
'intelligence.agents.refresh': 'Refresh',
|
||||
'intelligence.agents.loading': 'Loading agents...',
|
||||
'intelligence.agents.failedToLoad': 'Could not load agents',
|
||||
'intelligence.agents.empty': 'No runnable agents are available.',
|
||||
'intelligence.agents.readOnly': 'Read-only',
|
||||
'intelligence.agents.writeCapable': 'Write-capable',
|
||||
'intelligence.agents.allTools': 'All tools',
|
||||
'intelligence.agents.toolCountOne': '{count} tool',
|
||||
'intelligence.agents.toolCountOther': '{count} tools',
|
||||
'intelligence.agents.subagentCountOne': '{count} subagent',
|
||||
'intelligence.agents.subagentCountOther': '{count} subagents',
|
||||
'intelligence.agents.startChat': 'Start chat',
|
||||
'intelligence.agents.startChatPrompt':
|
||||
'Start a chat with this agent. Introduce your specialty, ask what you need to know, and wait for my task.',
|
||||
'intelligence.agents.copyId': 'Copy ID',
|
||||
'intelligence.agents.copied': 'Copied',
|
||||
'intelligence.agents.taskPlaceholder': 'Task for this agent',
|
||||
'intelligence.agents.runTask': 'Run task',
|
||||
'intelligence.agents.running': 'Running...',
|
||||
'intelligence.agents.runFailed': 'Could not start the selected agent',
|
||||
'intelligence.agents.model.inherit': 'Inherit',
|
||||
'intelligence.agents.tier.chat': 'Chat',
|
||||
'intelligence.agents.tier.reasoning': 'Reasoning',
|
||||
'intelligence.agents.tier.worker': 'Worker',
|
||||
'modelCouncil.title': 'Model Council',
|
||||
'modelCouncil.intro':
|
||||
'Ask one question, get independent answers from several models in parallel, then a chair model synthesizes where they agree, where they disagree, and what each uniquely adds.',
|
||||
|
||||
@@ -4656,6 +4656,33 @@ const messages: TranslationMap = {
|
||||
'autocomplete.maxChars': 'Máximo de caracteres de contexto',
|
||||
'autocomplete.overlayTtlMs': 'Tiempo de espera de superposición (ms)',
|
||||
'memory.tab.council': 'Council',
|
||||
'intelligence.agents.title': 'Biblioteca de agentes',
|
||||
'intelligence.agents.subtitle':
|
||||
'Revisa especialistas ejecutables y envía una tarea a un agente concreto.',
|
||||
'intelligence.agents.refresh': 'Actualizar',
|
||||
'intelligence.agents.loading': 'Cargando agentes…',
|
||||
'intelligence.agents.failedToLoad': 'No se pudieron cargar los agentes',
|
||||
'intelligence.agents.empty': 'No hay agentes ejecutables disponibles.',
|
||||
'intelligence.agents.readOnly': 'Solo lectura',
|
||||
'intelligence.agents.writeCapable': 'Puede escribir',
|
||||
'intelligence.agents.allTools': 'Todas las herramientas',
|
||||
'intelligence.agents.toolCountOne': '{count} herramienta',
|
||||
'intelligence.agents.toolCountOther': '{count} herramientas',
|
||||
'intelligence.agents.subagentCountOne': '{count} subagente',
|
||||
'intelligence.agents.subagentCountOther': '{count} subagentes',
|
||||
'intelligence.agents.startChat': 'Iniciar chat',
|
||||
'intelligence.agents.startChatPrompt':
|
||||
'Inicia un chat con este agente. Presenta tu especialidad, pregunta qué necesitas saber y espera mi tarea.',
|
||||
'intelligence.agents.copyId': 'Copiar ID',
|
||||
'intelligence.agents.copied': 'Copiado',
|
||||
'intelligence.agents.taskPlaceholder': 'Tarea para este agente',
|
||||
'intelligence.agents.runTask': 'Ejecutar tarea',
|
||||
'intelligence.agents.running': 'Ejecutando…',
|
||||
'intelligence.agents.runFailed': 'No se pudo iniciar el agente seleccionado',
|
||||
'intelligence.agents.model.inherit': 'Heredar',
|
||||
'intelligence.agents.tier.chat': 'Chat',
|
||||
'intelligence.agents.tier.reasoning': 'Razonamiento',
|
||||
'intelligence.agents.tier.worker': 'Trabajador',
|
||||
'modelCouncil.title': 'Model Council',
|
||||
'modelCouncil.intro':
|
||||
'Ask one question, get independent answers from several models in parallel, then a chair model synthesizes where they agree, where they disagree, and what each uniquely adds.',
|
||||
|
||||
@@ -4671,6 +4671,33 @@ const messages: TranslationMap = {
|
||||
'autocomplete.maxChars': 'Caractères de contexte maximum',
|
||||
'autocomplete.overlayTtlMs': "Délai d'affichage (ms)",
|
||||
'memory.tab.council': 'Council',
|
||||
'intelligence.agents.title': "Bibliothèque d'agents",
|
||||
'intelligence.agents.subtitle':
|
||||
'Inspectez les spécialistes exécutables et envoyez une tâche à un agent nommé.',
|
||||
'intelligence.agents.refresh': 'Actualiser',
|
||||
'intelligence.agents.loading': 'Chargement des agents…',
|
||||
'intelligence.agents.failedToLoad': 'Impossible de charger les agents',
|
||||
'intelligence.agents.empty': 'Aucun agent exécutable disponible.',
|
||||
'intelligence.agents.readOnly': 'Lecture seule',
|
||||
'intelligence.agents.writeCapable': 'Peut écrire',
|
||||
'intelligence.agents.allTools': 'Tous les outils',
|
||||
'intelligence.agents.toolCountOne': '{count} outil',
|
||||
'intelligence.agents.toolCountOther': '{count} outils',
|
||||
'intelligence.agents.subagentCountOne': '{count} sous-agent',
|
||||
'intelligence.agents.subagentCountOther': '{count} sous-agents',
|
||||
'intelligence.agents.startChat': 'Démarrer le chat',
|
||||
'intelligence.agents.startChatPrompt':
|
||||
'Démarre une conversation avec cet agent. Présente ta spécialité, demande le contexte nécessaire, puis attends ma tâche.',
|
||||
'intelligence.agents.copyId': "Copier l'ID",
|
||||
'intelligence.agents.copied': 'Copié',
|
||||
'intelligence.agents.taskPlaceholder': 'Tâche pour cet agent',
|
||||
'intelligence.agents.runTask': 'Exécuter la tâche',
|
||||
'intelligence.agents.running': 'Exécution…',
|
||||
'intelligence.agents.runFailed': "Impossible de démarrer l'agent sélectionné",
|
||||
'intelligence.agents.model.inherit': 'Hériter',
|
||||
'intelligence.agents.tier.chat': 'Chat',
|
||||
'intelligence.agents.tier.reasoning': 'Raisonnement',
|
||||
'intelligence.agents.tier.worker': 'Travailleur',
|
||||
'modelCouncil.title': 'Model Council',
|
||||
'modelCouncil.intro':
|
||||
'Ask one question, get independent answers from several models in parallel, then a chair model synthesizes where they agree, where they disagree, and what each uniquely adds.',
|
||||
|
||||
@@ -4572,6 +4572,33 @@ const messages: TranslationMap = {
|
||||
'autocomplete.maxChars': 'अधिकतम संदर्भ वर्ण',
|
||||
'autocomplete.overlayTtlMs': 'ओवरले समय-समाप्ति (ms)',
|
||||
'memory.tab.council': 'Council',
|
||||
'intelligence.agents.title': 'एजेंट लाइब्रेरी',
|
||||
'intelligence.agents.subtitle':
|
||||
'चलाए जा सकने वाले विशेषज्ञ देखें और किसी नामित एजेंट को एक काम भेजें।',
|
||||
'intelligence.agents.refresh': 'रीफ्रेश',
|
||||
'intelligence.agents.loading': 'एजेंट लोड हो रहे हैं…',
|
||||
'intelligence.agents.failedToLoad': 'एजेंट लोड नहीं हो सके',
|
||||
'intelligence.agents.empty': 'कोई चलाने योग्य एजेंट उपलब्ध नहीं है।',
|
||||
'intelligence.agents.readOnly': 'केवल पढ़ें',
|
||||
'intelligence.agents.writeCapable': 'लिख सकता है',
|
||||
'intelligence.agents.allTools': 'सभी टूल',
|
||||
'intelligence.agents.toolCountOne': '{count} टूल',
|
||||
'intelligence.agents.toolCountOther': '{count} टूल',
|
||||
'intelligence.agents.subagentCountOne': '{count} सब-एजेंट',
|
||||
'intelligence.agents.subagentCountOther': '{count} सब-एजेंट',
|
||||
'intelligence.agents.startChat': 'चैट शुरू करें',
|
||||
'intelligence.agents.startChatPrompt':
|
||||
'इस एजेंट के साथ चैट शुरू करें। अपनी विशेषज्ञता बताएं, जरूरी जानकारी पूछें, और मेरे काम का इंतज़ार करें।',
|
||||
'intelligence.agents.copyId': 'ID कॉपी करें',
|
||||
'intelligence.agents.copied': 'कॉपी हो गया',
|
||||
'intelligence.agents.taskPlaceholder': 'इस एजेंट के लिए काम',
|
||||
'intelligence.agents.runTask': 'काम चलाएं',
|
||||
'intelligence.agents.running': 'चल रहा है…',
|
||||
'intelligence.agents.runFailed': 'चुना गया एजेंट शुरू नहीं हो सका',
|
||||
'intelligence.agents.model.inherit': 'इनहेरिट',
|
||||
'intelligence.agents.tier.chat': 'चैट',
|
||||
'intelligence.agents.tier.reasoning': 'तर्क',
|
||||
'intelligence.agents.tier.worker': 'वर्कर',
|
||||
'modelCouncil.title': 'Model Council',
|
||||
'modelCouncil.intro':
|
||||
'Ask one question, get independent answers from several models in parallel, then a chair model synthesizes where they agree, where they disagree, and what each uniquely adds.',
|
||||
|
||||
@@ -4580,6 +4580,33 @@ const messages: TranslationMap = {
|
||||
'autocomplete.maxChars': 'Karakter konteks maks',
|
||||
'autocomplete.overlayTtlMs': 'Batas waktu overlay (md)',
|
||||
'memory.tab.council': 'Council',
|
||||
'intelligence.agents.title': 'Pustaka Agen',
|
||||
'intelligence.agents.subtitle':
|
||||
'Periksa spesialis yang dapat dijalankan dan kirim satu tugas ke agen tertentu.',
|
||||
'intelligence.agents.refresh': 'Segarkan',
|
||||
'intelligence.agents.loading': 'Memuat agen…',
|
||||
'intelligence.agents.failedToLoad': 'Tidak dapat memuat agen',
|
||||
'intelligence.agents.empty': 'Tidak ada agen yang dapat dijalankan.',
|
||||
'intelligence.agents.readOnly': 'Hanya baca',
|
||||
'intelligence.agents.writeCapable': 'Dapat menulis',
|
||||
'intelligence.agents.allTools': 'Semua alat',
|
||||
'intelligence.agents.toolCountOne': '{count} alat',
|
||||
'intelligence.agents.toolCountOther': '{count} alat',
|
||||
'intelligence.agents.subagentCountOne': '{count} subagen',
|
||||
'intelligence.agents.subagentCountOther': '{count} subagen',
|
||||
'intelligence.agents.startChat': 'Mulai chat',
|
||||
'intelligence.agents.startChatPrompt':
|
||||
'Mulai chat dengan agen ini. Perkenalkan spesialisasimu, tanyakan konteks yang diperlukan, lalu tunggu tugas saya.',
|
||||
'intelligence.agents.copyId': 'Salin ID',
|
||||
'intelligence.agents.copied': 'Disalin',
|
||||
'intelligence.agents.taskPlaceholder': 'Tugas untuk agen ini',
|
||||
'intelligence.agents.runTask': 'Jalankan tugas',
|
||||
'intelligence.agents.running': 'Berjalan…',
|
||||
'intelligence.agents.runFailed': 'Tidak dapat memulai agen yang dipilih',
|
||||
'intelligence.agents.model.inherit': 'Warisi',
|
||||
'intelligence.agents.tier.chat': 'Chat',
|
||||
'intelligence.agents.tier.reasoning': 'Penalaran',
|
||||
'intelligence.agents.tier.worker': 'Pekerja',
|
||||
'modelCouncil.title': 'Model Council',
|
||||
'modelCouncil.intro':
|
||||
'Ask one question, get independent answers from several models in parallel, then a chair model synthesizes where they agree, where they disagree, and what each uniquely adds.',
|
||||
|
||||
@@ -4645,6 +4645,33 @@ const messages: TranslationMap = {
|
||||
'autocomplete.maxChars': 'Caratteri massimi di contesto',
|
||||
'autocomplete.overlayTtlMs': 'Timeout overlay (ms)',
|
||||
'memory.tab.council': 'Council',
|
||||
'intelligence.agents.title': 'Libreria agenti',
|
||||
'intelligence.agents.subtitle':
|
||||
'Ispeziona specialisti eseguibili e invia un compito a un agente specifico.',
|
||||
'intelligence.agents.refresh': 'Aggiorna',
|
||||
'intelligence.agents.loading': 'Caricamento agenti…',
|
||||
'intelligence.agents.failedToLoad': 'Impossibile caricare gli agenti',
|
||||
'intelligence.agents.empty': 'Nessun agente eseguibile disponibile.',
|
||||
'intelligence.agents.readOnly': 'Sola lettura',
|
||||
'intelligence.agents.writeCapable': 'Può scrivere',
|
||||
'intelligence.agents.allTools': 'Tutti gli strumenti',
|
||||
'intelligence.agents.toolCountOne': '{count} strumento',
|
||||
'intelligence.agents.toolCountOther': '{count} strumenti',
|
||||
'intelligence.agents.subagentCountOne': '{count} sottoagente',
|
||||
'intelligence.agents.subagentCountOther': '{count} sottoagenti',
|
||||
'intelligence.agents.startChat': 'Avvia chat',
|
||||
'intelligence.agents.startChatPrompt':
|
||||
'Avvia una chat con questo agente. Presenta la tua specialità, chiedi cosa devi sapere e attendi il mio compito.',
|
||||
'intelligence.agents.copyId': 'Copia ID',
|
||||
'intelligence.agents.copied': 'Copiato',
|
||||
'intelligence.agents.taskPlaceholder': 'Compito per questo agente',
|
||||
'intelligence.agents.runTask': 'Esegui compito',
|
||||
'intelligence.agents.running': 'In esecuzione…',
|
||||
'intelligence.agents.runFailed': 'Impossibile avviare l’agente selezionato',
|
||||
'intelligence.agents.model.inherit': 'Eredita',
|
||||
'intelligence.agents.tier.chat': 'Chat',
|
||||
'intelligence.agents.tier.reasoning': 'Ragionamento',
|
||||
'intelligence.agents.tier.worker': 'Worker',
|
||||
'modelCouncil.title': 'Model Council',
|
||||
'modelCouncil.intro':
|
||||
'Ask one question, get independent answers from several models in parallel, then a chair model synthesizes where they agree, where they disagree, and what each uniquely adds.',
|
||||
|
||||
@@ -4521,6 +4521,33 @@ const messages: TranslationMap = {
|
||||
'autocomplete.maxChars': '최대 컨텍스트 문자 수',
|
||||
'autocomplete.overlayTtlMs': '오버레이 시간 초과 (ms)',
|
||||
'memory.tab.council': 'Council',
|
||||
'intelligence.agents.title': '에이전트 라이브러리',
|
||||
'intelligence.agents.subtitle':
|
||||
'실행 가능한 전문가를 살펴보고 지정한 에이전트에 작업을 보냅니다.',
|
||||
'intelligence.agents.refresh': '새로고침',
|
||||
'intelligence.agents.loading': '에이전트를 불러오는 중…',
|
||||
'intelligence.agents.failedToLoad': '에이전트를 불러올 수 없습니다',
|
||||
'intelligence.agents.empty': '실행 가능한 에이전트가 없습니다.',
|
||||
'intelligence.agents.readOnly': '읽기 전용',
|
||||
'intelligence.agents.writeCapable': '쓰기 가능',
|
||||
'intelligence.agents.allTools': '모든 도구',
|
||||
'intelligence.agents.toolCountOne': '도구 {count}개',
|
||||
'intelligence.agents.toolCountOther': '도구 {count}개',
|
||||
'intelligence.agents.subagentCountOne': '하위 에이전트 {count}개',
|
||||
'intelligence.agents.subagentCountOther': '하위 에이전트 {count}개',
|
||||
'intelligence.agents.startChat': '채팅 시작',
|
||||
'intelligence.agents.startChatPrompt':
|
||||
'이 에이전트와 채팅을 시작하세요. 전문 분야를 소개하고 필요한 정보를 물은 뒤 내 작업을 기다리세요.',
|
||||
'intelligence.agents.copyId': 'ID 복사',
|
||||
'intelligence.agents.copied': '복사됨',
|
||||
'intelligence.agents.taskPlaceholder': '이 에이전트에게 줄 작업',
|
||||
'intelligence.agents.runTask': '작업 실행',
|
||||
'intelligence.agents.running': '실행 중…',
|
||||
'intelligence.agents.runFailed': '선택한 에이전트를 시작할 수 없습니다',
|
||||
'intelligence.agents.model.inherit': '상속',
|
||||
'intelligence.agents.tier.chat': '채팅',
|
||||
'intelligence.agents.tier.reasoning': '추론',
|
||||
'intelligence.agents.tier.worker': '작업자',
|
||||
'modelCouncil.title': 'Model Council',
|
||||
'modelCouncil.intro':
|
||||
'Ask one question, get independent answers from several models in parallel, then a chair model synthesizes where they agree, where they disagree, and what each uniquely adds.',
|
||||
|
||||
@@ -4609,6 +4609,33 @@ const messages: TranslationMap = {
|
||||
'autocomplete.maxChars': 'Maksymalna liczba znaków kontekstu',
|
||||
'autocomplete.overlayTtlMs': 'Limit czasu nakładki (ms)',
|
||||
'memory.tab.council': 'Council',
|
||||
'intelligence.agents.title': 'Biblioteka agentów',
|
||||
'intelligence.agents.subtitle':
|
||||
'Przeglądaj uruchamialnych specjalistów i wyślij zadanie do wskazanego agenta.',
|
||||
'intelligence.agents.refresh': 'Odśwież',
|
||||
'intelligence.agents.loading': 'Ładowanie agentów…',
|
||||
'intelligence.agents.failedToLoad': 'Nie udało się załadować agentów',
|
||||
'intelligence.agents.empty': 'Brak dostępnych agentów do uruchomienia.',
|
||||
'intelligence.agents.readOnly': 'Tylko odczyt',
|
||||
'intelligence.agents.writeCapable': 'Może zapisywać',
|
||||
'intelligence.agents.allTools': 'Wszystkie narzędzia',
|
||||
'intelligence.agents.toolCountOne': '{count} narzędzie',
|
||||
'intelligence.agents.toolCountOther': '{count} narzędzi',
|
||||
'intelligence.agents.subagentCountOne': '{count} subagent',
|
||||
'intelligence.agents.subagentCountOther': '{count} subagentów',
|
||||
'intelligence.agents.startChat': 'Rozpocznij czat',
|
||||
'intelligence.agents.startChatPrompt':
|
||||
'Rozpocznij czat z tym agentem. Przedstaw swoją specjalizację, zapytaj o potrzebny kontekst i poczekaj na moje zadanie.',
|
||||
'intelligence.agents.copyId': 'Kopiuj ID',
|
||||
'intelligence.agents.copied': 'Skopiowano',
|
||||
'intelligence.agents.taskPlaceholder': 'Zadanie dla tego agenta',
|
||||
'intelligence.agents.runTask': 'Uruchom zadanie',
|
||||
'intelligence.agents.running': 'Uruchamianie…',
|
||||
'intelligence.agents.runFailed': 'Nie udało się uruchomić wybranego agenta',
|
||||
'intelligence.agents.model.inherit': 'Dziedzicz',
|
||||
'intelligence.agents.tier.chat': 'Czat',
|
||||
'intelligence.agents.tier.reasoning': 'Rozumowanie',
|
||||
'intelligence.agents.tier.worker': 'Wykonawca',
|
||||
'modelCouncil.title': 'Model Council',
|
||||
'modelCouncil.intro':
|
||||
'Ask one question, get independent answers from several models in parallel, then a chair model synthesizes where they agree, where they disagree, and what each uniquely adds.',
|
||||
|
||||
@@ -4642,6 +4642,33 @@ const messages: TranslationMap = {
|
||||
'autocomplete.maxChars': 'Máximo de caracteres de contexto',
|
||||
'autocomplete.overlayTtlMs': 'Tempo limite da sobreposição (ms)',
|
||||
'memory.tab.council': 'Council',
|
||||
'intelligence.agents.title': 'Biblioteca de agentes',
|
||||
'intelligence.agents.subtitle':
|
||||
'Inspecione especialistas executáveis e envie uma tarefa para um agente nomeado.',
|
||||
'intelligence.agents.refresh': 'Atualizar',
|
||||
'intelligence.agents.loading': 'Carregando agentes…',
|
||||
'intelligence.agents.failedToLoad': 'Não foi possível carregar os agentes',
|
||||
'intelligence.agents.empty': 'Nenhum agente executável disponível.',
|
||||
'intelligence.agents.readOnly': 'Somente leitura',
|
||||
'intelligence.agents.writeCapable': 'Pode escrever',
|
||||
'intelligence.agents.allTools': 'Todas as ferramentas',
|
||||
'intelligence.agents.toolCountOne': '{count} ferramenta',
|
||||
'intelligence.agents.toolCountOther': '{count} ferramentas',
|
||||
'intelligence.agents.subagentCountOne': '{count} subagente',
|
||||
'intelligence.agents.subagentCountOther': '{count} subagentes',
|
||||
'intelligence.agents.startChat': 'Iniciar chat',
|
||||
'intelligence.agents.startChatPrompt':
|
||||
'Inicie um chat com este agente. Apresente sua especialidade, pergunte o que precisa saber e aguarde minha tarefa.',
|
||||
'intelligence.agents.copyId': 'Copiar ID',
|
||||
'intelligence.agents.copied': 'Copiado',
|
||||
'intelligence.agents.taskPlaceholder': 'Tarefa para este agente',
|
||||
'intelligence.agents.runTask': 'Executar tarefa',
|
||||
'intelligence.agents.running': 'Executando…',
|
||||
'intelligence.agents.runFailed': 'Não foi possível iniciar o agente selecionado',
|
||||
'intelligence.agents.model.inherit': 'Herdar',
|
||||
'intelligence.agents.tier.chat': 'Chat',
|
||||
'intelligence.agents.tier.reasoning': 'Raciocínio',
|
||||
'intelligence.agents.tier.worker': 'Trabalhador',
|
||||
'modelCouncil.title': 'Model Council',
|
||||
'modelCouncil.intro':
|
||||
'Ask one question, get independent answers from several models in parallel, then a chair model synthesizes where they agree, where they disagree, and what each uniquely adds.',
|
||||
|
||||
@@ -4609,6 +4609,33 @@ const messages: TranslationMap = {
|
||||
'autocomplete.maxChars': 'Макс. символов контекста',
|
||||
'autocomplete.overlayTtlMs': 'Тайм-аут наложения (мс)',
|
||||
'memory.tab.council': 'Council',
|
||||
'intelligence.agents.title': 'Библиотека агентов',
|
||||
'intelligence.agents.subtitle':
|
||||
'Просматривайте доступных специалистов и отправляйте задачу выбранному агенту.',
|
||||
'intelligence.agents.refresh': 'Обновить',
|
||||
'intelligence.agents.loading': 'Загрузка агентов…',
|
||||
'intelligence.agents.failedToLoad': 'Не удалось загрузить агентов',
|
||||
'intelligence.agents.empty': 'Нет доступных запускаемых агентов.',
|
||||
'intelligence.agents.readOnly': 'Только чтение',
|
||||
'intelligence.agents.writeCapable': 'Может изменять',
|
||||
'intelligence.agents.allTools': 'Все инструменты',
|
||||
'intelligence.agents.toolCountOne': '{count} инструмент',
|
||||
'intelligence.agents.toolCountOther': '{count} инструментов',
|
||||
'intelligence.agents.subagentCountOne': '{count} субагент',
|
||||
'intelligence.agents.subagentCountOther': '{count} субагентов',
|
||||
'intelligence.agents.startChat': 'Начать чат',
|
||||
'intelligence.agents.startChatPrompt':
|
||||
'Начни чат с этим агентом. Представь свою специализацию, спроси нужный контекст и жди мою задачу.',
|
||||
'intelligence.agents.copyId': 'Копировать ID',
|
||||
'intelligence.agents.copied': 'Скопировано',
|
||||
'intelligence.agents.taskPlaceholder': 'Задача для этого агента',
|
||||
'intelligence.agents.runTask': 'Запустить задачу',
|
||||
'intelligence.agents.running': 'Выполняется…',
|
||||
'intelligence.agents.runFailed': 'Не удалось запустить выбранного агента',
|
||||
'intelligence.agents.model.inherit': 'Наследовать',
|
||||
'intelligence.agents.tier.chat': 'Чат',
|
||||
'intelligence.agents.tier.reasoning': 'Рассуждение',
|
||||
'intelligence.agents.tier.worker': 'Исполнитель',
|
||||
'modelCouncil.title': 'Model Council',
|
||||
'modelCouncil.intro':
|
||||
'Ask one question, get independent answers from several models in parallel, then a chair model synthesizes where they agree, where they disagree, and what each uniquely adds.',
|
||||
|
||||
@@ -4341,6 +4341,32 @@ const messages: TranslationMap = {
|
||||
'autocomplete.maxChars': '最大上下文字符数',
|
||||
'autocomplete.overlayTtlMs': '覆盖层超时 (ms)',
|
||||
'memory.tab.council': 'Council',
|
||||
'intelligence.agents.title': '智能体库',
|
||||
'intelligence.agents.subtitle': '查看可运行的专家,并把一个任务交给指定智能体。',
|
||||
'intelligence.agents.refresh': '刷新',
|
||||
'intelligence.agents.loading': '正在加载智能体…',
|
||||
'intelligence.agents.failedToLoad': '无法加载智能体',
|
||||
'intelligence.agents.empty': '暂无可运行的智能体。',
|
||||
'intelligence.agents.readOnly': '只读',
|
||||
'intelligence.agents.writeCapable': '可写入',
|
||||
'intelligence.agents.allTools': '全部工具',
|
||||
'intelligence.agents.toolCountOne': '{count} 个工具',
|
||||
'intelligence.agents.toolCountOther': '{count} 个工具',
|
||||
'intelligence.agents.subagentCountOne': '{count} 个子智能体',
|
||||
'intelligence.agents.subagentCountOther': '{count} 个子智能体',
|
||||
'intelligence.agents.startChat': '开始聊天',
|
||||
'intelligence.agents.startChatPrompt':
|
||||
'用这个智能体开始聊天。介绍你的专长,询问需要了解的信息,然后等待我的任务。',
|
||||
'intelligence.agents.copyId': '复制 ID',
|
||||
'intelligence.agents.copied': '已复制',
|
||||
'intelligence.agents.taskPlaceholder': '给这个智能体的任务',
|
||||
'intelligence.agents.runTask': '运行任务',
|
||||
'intelligence.agents.running': '正在运行…',
|
||||
'intelligence.agents.runFailed': '无法启动所选智能体',
|
||||
'intelligence.agents.model.inherit': '继承',
|
||||
'intelligence.agents.tier.chat': '聊天',
|
||||
'intelligence.agents.tier.reasoning': '推理',
|
||||
'intelligence.agents.tier.worker': '工作者',
|
||||
'modelCouncil.title': 'Model Council',
|
||||
'modelCouncil.intro':
|
||||
'Ask one question, get independent answers from several models in parallel, then a chair model synthesizes where they agree, where they disagree, and what each uniquely adds.',
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { callCoreRpc } from '../coreRpcClient';
|
||||
import { agentLibraryApi } from './agentLibraryApi';
|
||||
|
||||
vi.mock('../coreRpcClient', () => ({ callCoreRpc: vi.fn() }));
|
||||
|
||||
const mockCall = vi.mocked(callCoreRpc);
|
||||
|
||||
describe('agentLibraryApi', () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it('lists safe agent definitions through the agent controller', async () => {
|
||||
mockCall.mockResolvedValueOnce({
|
||||
definitions: [
|
||||
{
|
||||
id: 'researcher',
|
||||
display_name: 'Researcher',
|
||||
when_to_use: 'Use for research.',
|
||||
tier: 'worker',
|
||||
model: { kind: 'hint', value: 'reasoning' },
|
||||
direct_tool_count: 1,
|
||||
direct_tool_names: ['web_search'],
|
||||
uses_wildcard_tools: false,
|
||||
subagent_ids: [],
|
||||
includes_profile: false,
|
||||
includes_memory_md: false,
|
||||
includes_memory_context: false,
|
||||
can_run_as_user_facing_worker: true,
|
||||
write_capable: false,
|
||||
source: 'builtin',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await expect(agentLibraryApi.listDefinitions()).resolves.toMatchObject([
|
||||
{ id: 'researcher', write_capable: false },
|
||||
]);
|
||||
expect(mockCall).toHaveBeenCalledWith({
|
||||
method: 'openhuman.agent_list_definitions',
|
||||
params: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('tolerates a missing definitions field', async () => {
|
||||
mockCall.mockResolvedValueOnce({});
|
||||
await expect(agentLibraryApi.listDefinitions()).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('tolerates a non-array definitions field', async () => {
|
||||
mockCall.mockResolvedValueOnce({ definitions: { id: 'bad-shape' } });
|
||||
await expect(agentLibraryApi.listDefinitions()).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import debug from 'debug';
|
||||
|
||||
import { callCoreRpc } from '../coreRpcClient';
|
||||
|
||||
const log = debug('agentLibraryApi');
|
||||
|
||||
export type AgentDefinitionSource = 'builtin' | 'custom';
|
||||
|
||||
export interface AgentDefinitionModel {
|
||||
kind: 'inherit' | 'exact' | 'hint' | string;
|
||||
value?: string | null;
|
||||
}
|
||||
|
||||
export interface AgentDefinitionDisplay {
|
||||
id: string;
|
||||
display_name: string;
|
||||
when_to_use: string;
|
||||
tier: 'chat' | 'reasoning' | 'worker' | string;
|
||||
model: AgentDefinitionModel;
|
||||
direct_tool_count: number;
|
||||
direct_tool_names: string[];
|
||||
uses_wildcard_tools: boolean;
|
||||
subagent_ids: string[];
|
||||
includes_profile: boolean;
|
||||
includes_memory_md: boolean;
|
||||
includes_memory_context: boolean;
|
||||
can_run_as_user_facing_worker: boolean;
|
||||
write_capable: boolean;
|
||||
source: AgentDefinitionSource;
|
||||
}
|
||||
|
||||
export const agentLibraryApi = {
|
||||
listDefinitions: async (): Promise<AgentDefinitionDisplay[]> => {
|
||||
log('[agent-library] listDefinitions entry');
|
||||
const response = await callCoreRpc<{ definitions?: AgentDefinitionDisplay[] }>({
|
||||
method: 'openhuman.agent_list_definitions',
|
||||
params: {},
|
||||
});
|
||||
const definitions = Array.isArray(response?.definitions) ? response.definitions : [];
|
||||
log('[agent-library] listDefinitions exit count=%d', definitions.length);
|
||||
return definitions;
|
||||
},
|
||||
};
|
||||
@@ -468,6 +468,16 @@ pub(super) const CAPABILITIES: &[Capability] = &[
|
||||
status: CapabilityStatus::Beta,
|
||||
privacy: DERIVED_TO_BACKEND,
|
||||
},
|
||||
Capability {
|
||||
id: "intelligence.agent_library",
|
||||
name: "Agents Library",
|
||||
domain: "intelligence",
|
||||
category: CapabilityCategory::Intelligence,
|
||||
description: "Browse safe display metadata for registered agent definitions, compare worker capabilities, and start a one-off task with an explicitly selected agent.",
|
||||
how_to: "Intelligence > Agent Tasks > Agents Library",
|
||||
status: CapabilityStatus::Beta,
|
||||
privacy: DERIVED_TO_BACKEND,
|
||||
},
|
||||
Capability {
|
||||
id: "intelligence.slack_memory_ingest",
|
||||
name: "Slack Memory Ingestion",
|
||||
|
||||
@@ -103,6 +103,7 @@ fn catalog_includes_additional_user_facing_surfaces() {
|
||||
"intelligence.mcp_server",
|
||||
"intelligence.searxng_search",
|
||||
"intelligence.tool_registry",
|
||||
"intelligence.agent_library",
|
||||
"intelligence.embedding_provider_config",
|
||||
"intelligence.embedding_provider_test",
|
||||
"intelligence.github_repo_memory_source",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
//! Safe, user-facing agent library projection.
|
||||
|
||||
mod ops;
|
||||
mod types;
|
||||
|
||||
pub use ops::{list_definition_metadata, metadata_from_definition};
|
||||
pub use types::{AgentDefinitionDisplay, AgentDefinitionModel, AgentDefinitionSource};
|
||||
@@ -0,0 +1,195 @@
|
||||
use crate::openhuman::agent::harness::definition::{
|
||||
AgentDefinition, AgentDefinitionRegistry, AgentTier, DefinitionSource, ModelSpec, SandboxMode,
|
||||
SubagentEntry, ToolScope,
|
||||
};
|
||||
use crate::openhuman::config::rpc as config_rpc;
|
||||
|
||||
use super::types::{AgentDefinitionDisplay, AgentDefinitionModel, AgentDefinitionSource};
|
||||
|
||||
pub async fn list_definition_metadata() -> Result<Vec<AgentDefinitionDisplay>, String> {
|
||||
tracing::debug!("[rpc][agent_library][entry] list_definitions");
|
||||
if AgentDefinitionRegistry::global().is_none() {
|
||||
let config = config_rpc::load_config_with_timeout().await?;
|
||||
AgentDefinitionRegistry::init_global(&config.workspace_dir)
|
||||
.map_err(|e| format!("failed to initialise AgentDefinitionRegistry: {e}"))?;
|
||||
}
|
||||
let registry = AgentDefinitionRegistry::global()
|
||||
.ok_or_else(|| "AgentDefinitionRegistry not initialised".to_string())?;
|
||||
let definitions = registry
|
||||
.list()
|
||||
.into_iter()
|
||||
.map(metadata_from_definition)
|
||||
.collect::<Vec<_>>();
|
||||
tracing::debug!(
|
||||
count = definitions.len(),
|
||||
"[rpc][agent_library][exit] list_definitions"
|
||||
);
|
||||
Ok(definitions)
|
||||
}
|
||||
|
||||
pub fn metadata_from_definition(def: &AgentDefinition) -> AgentDefinitionDisplay {
|
||||
let direct_tool_names = direct_tool_names(def);
|
||||
let uses_wildcard_tools = matches!(def.tools, ToolScope::Wildcard);
|
||||
let subagent_ids = def
|
||||
.subagents
|
||||
.iter()
|
||||
.filter_map(|entry| match entry {
|
||||
SubagentEntry::AgentId(id) => Some(id.clone()),
|
||||
SubagentEntry::Skills(_) => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let can_run_as_user_facing_worker = def.id != "orchestrator"
|
||||
&& matches!(def.agent_tier, AgentTier::Reasoning | AgentTier::Worker);
|
||||
|
||||
AgentDefinitionDisplay {
|
||||
id: def.id.clone(),
|
||||
display_name: def.display_name().to_string(),
|
||||
when_to_use: def.when_to_use.clone(),
|
||||
tier: def.agent_tier.as_str().to_string(),
|
||||
model: model_metadata(&def.model),
|
||||
tools: def.tools.clone(),
|
||||
direct_tool_count: direct_tool_names.len(),
|
||||
direct_tool_names,
|
||||
uses_wildcard_tools,
|
||||
subagent_ids,
|
||||
includes_profile: !def.omit_profile,
|
||||
includes_memory_md: !def.omit_memory_md,
|
||||
includes_memory_context: !def.omit_memory_context,
|
||||
can_run_as_user_facing_worker,
|
||||
write_capable: is_write_capable(def),
|
||||
source: match &def.source {
|
||||
DefinitionSource::Builtin => AgentDefinitionSource::Builtin,
|
||||
DefinitionSource::File(_) => AgentDefinitionSource::Custom,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn model_metadata(model: &ModelSpec) -> AgentDefinitionModel {
|
||||
match model {
|
||||
ModelSpec::Inherit => AgentDefinitionModel {
|
||||
kind: "inherit".to_string(),
|
||||
value: None,
|
||||
},
|
||||
ModelSpec::Exact(value) => AgentDefinitionModel {
|
||||
kind: "exact".to_string(),
|
||||
value: Some(value.clone()),
|
||||
},
|
||||
ModelSpec::Hint(value) => AgentDefinitionModel {
|
||||
kind: "hint".to_string(),
|
||||
value: Some(value.clone()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn direct_tool_names(def: &AgentDefinition) -> Vec<String> {
|
||||
let mut names = match &def.tools {
|
||||
ToolScope::Wildcard => Vec::new(),
|
||||
ToolScope::Named(names) => names.clone(),
|
||||
};
|
||||
for tool in &def.extra_tools {
|
||||
if !names.contains(tool) {
|
||||
names.push(tool.clone());
|
||||
}
|
||||
}
|
||||
names.retain(|name| !def.disallowed_tools.contains(name));
|
||||
names.sort();
|
||||
names
|
||||
}
|
||||
|
||||
fn is_write_capable(def: &AgentDefinition) -> bool {
|
||||
if matches!(def.sandbox_mode, SandboxMode::ReadOnly) {
|
||||
return false;
|
||||
}
|
||||
if matches!(def.tools, ToolScope::Wildcard) {
|
||||
return true;
|
||||
}
|
||||
direct_tool_names(def).iter().any(|name| {
|
||||
let normalized = name.to_ascii_lowercase();
|
||||
normalized.contains("write")
|
||||
|| normalized.contains("edit")
|
||||
|| normalized.contains("execute")
|
||||
|| normalized.contains("shell")
|
||||
|| normalized.contains("apply")
|
||||
|| normalized.contains("delete")
|
||||
|| normalized.contains("remove")
|
||||
|| normalized.contains("create")
|
||||
|| normalized.contains("update")
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::openhuman::agent::harness::definition::{PromptSource, SkillsWildcard};
|
||||
|
||||
fn definition() -> AgentDefinition {
|
||||
AgentDefinition {
|
||||
id: "researcher".to_string(),
|
||||
when_to_use: "Use for research.".to_string(),
|
||||
display_name: Some("Researcher".to_string()),
|
||||
system_prompt: PromptSource::Inline("hidden prompt".to_string()),
|
||||
omit_identity: true,
|
||||
omit_memory_context: true,
|
||||
omit_safety_preamble: true,
|
||||
omit_skills_catalog: true,
|
||||
omit_profile: false,
|
||||
omit_memory_md: true,
|
||||
model: ModelSpec::Hint("reasoning".to_string()),
|
||||
temperature: 0.2,
|
||||
tools: ToolScope::Named(vec!["web_search".to_string(), "file_read".to_string()]),
|
||||
disallowed_tools: vec!["file_read".to_string()],
|
||||
skill_filter: None,
|
||||
extra_tools: vec!["memory_search".to_string(), "web_search".to_string()],
|
||||
max_iterations: 8,
|
||||
iteration_policy: Default::default(),
|
||||
max_result_chars: None,
|
||||
timeout_secs: None,
|
||||
sandbox_mode: SandboxMode::ReadOnly,
|
||||
background: false,
|
||||
subagents: vec![
|
||||
SubagentEntry::AgentId("critic".to_string()),
|
||||
SubagentEntry::Skills(SkillsWildcard {
|
||||
skills: "*".to_string(),
|
||||
}),
|
||||
],
|
||||
delegate_name: None,
|
||||
agent_tier: AgentTier::Worker,
|
||||
source: DefinitionSource::Builtin,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_projection_omits_prompt_and_paths() {
|
||||
let display = metadata_from_definition(&definition());
|
||||
|
||||
assert_eq!(display.id, "researcher");
|
||||
assert_eq!(display.display_name, "Researcher");
|
||||
assert_eq!(display.model.kind, "hint");
|
||||
assert_eq!(display.model.value.as_deref(), Some("reasoning"));
|
||||
match &display.tools {
|
||||
ToolScope::Named(names) => {
|
||||
assert_eq!(
|
||||
names,
|
||||
&vec!["web_search".to_string(), "file_read".to_string()]
|
||||
);
|
||||
}
|
||||
ToolScope::Wildcard => panic!("expected named tool scope"),
|
||||
}
|
||||
assert_eq!(
|
||||
display.direct_tool_names,
|
||||
vec!["memory_search", "web_search"]
|
||||
);
|
||||
assert_eq!(display.direct_tool_count, 2);
|
||||
assert!(!display.uses_wildcard_tools);
|
||||
assert_eq!(display.subagent_ids, vec!["critic"]);
|
||||
assert!(display.includes_profile);
|
||||
assert!(!display.includes_memory_md);
|
||||
assert!(!display.includes_memory_context);
|
||||
assert!(display.can_run_as_user_facing_worker);
|
||||
assert!(!display.write_capable);
|
||||
|
||||
let json = serde_json::to_value(display).expect("serialize display");
|
||||
assert!(json.get("system_prompt").is_none());
|
||||
assert!(json.get("prompt").is_none());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::openhuman::agent::harness::definition::ToolScope;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AgentDefinitionSource {
|
||||
Builtin,
|
||||
Custom,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct AgentDefinitionModel {
|
||||
pub kind: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub value: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AgentDefinitionDisplay {
|
||||
pub id: String,
|
||||
pub display_name: String,
|
||||
pub when_to_use: String,
|
||||
pub tier: String,
|
||||
pub model: AgentDefinitionModel,
|
||||
pub tools: ToolScope,
|
||||
pub direct_tool_count: usize,
|
||||
pub direct_tool_names: Vec<String>,
|
||||
pub uses_wildcard_tools: bool,
|
||||
pub subagent_ids: Vec<String>,
|
||||
pub includes_profile: bool,
|
||||
pub includes_memory_md: bool,
|
||||
pub includes_memory_context: bool,
|
||||
pub can_run_as_user_facing_worker: bool,
|
||||
pub write_capable: bool,
|
||||
pub source: AgentDefinitionSource,
|
||||
}
|
||||
@@ -26,6 +26,7 @@ pub mod error;
|
||||
pub mod harness;
|
||||
pub mod hooks;
|
||||
pub mod host_runtime;
|
||||
pub mod library;
|
||||
pub mod memory_loader;
|
||||
pub mod multimodal;
|
||||
pub mod personality_paths;
|
||||
|
||||
@@ -114,10 +114,13 @@ pub fn schemas(function: &str) -> ControllerSchema {
|
||||
"list_definitions" => ControllerSchema {
|
||||
namespace: "agent",
|
||||
function: "list_definitions",
|
||||
description: "List all sub-agent definitions in the global registry \
|
||||
(built-ins + custom TOML files under <workspace>/agents/).",
|
||||
description:
|
||||
"List safe display metadata for sub-agent definitions in the global registry.",
|
||||
inputs: vec![],
|
||||
outputs: vec![json_output("definitions", "Array of AgentDefinition.")],
|
||||
outputs: vec![json_output(
|
||||
"definitions",
|
||||
"Array of safe AgentDefinitionDisplay payloads; prompt bodies are omitted.",
|
||||
)],
|
||||
},
|
||||
"get_definition" => ControllerSchema {
|
||||
namespace: "agent",
|
||||
@@ -273,9 +276,7 @@ fn handle_server_status(_params: Map<String, Value>) -> ControllerFuture {
|
||||
|
||||
fn handle_list_definitions(_params: Map<String, Value>) -> ControllerFuture {
|
||||
Box::pin(async {
|
||||
let registry = crate::openhuman::agent::harness::AgentDefinitionRegistry::global()
|
||||
.ok_or_else(|| "AgentDefinitionRegistry not initialised".to_string())?;
|
||||
let defs: Vec<&crate::openhuman::agent::harness::AgentDefinition> = registry.list();
|
||||
let defs = crate::openhuman::agent::library::list_definition_metadata().await?;
|
||||
Ok(serde_json::json!({ "definitions": defs }))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1081,6 +1081,41 @@ async fn json_rpc_agent_registry_manages_defaults_and_custom_agents() {
|
||||
Some(true)
|
||||
);
|
||||
|
||||
let definitions = post_json_rpc(
|
||||
&rpc_base,
|
||||
2862_1_1,
|
||||
"openhuman.agent_list_definitions",
|
||||
json!({}),
|
||||
)
|
||||
.await;
|
||||
let definitions_result = assert_no_jsonrpc_error(&definitions, "agent_list_definitions");
|
||||
let definitions = definitions_result
|
||||
.get("definitions")
|
||||
.and_then(Value::as_array)
|
||||
.expect("agent_list_definitions should return definitions array");
|
||||
let researcher = definitions
|
||||
.iter()
|
||||
.find(|definition| definition.get("id").and_then(Value::as_str) == Some("researcher"))
|
||||
.expect("safe agent library should include researcher");
|
||||
assert_eq!(
|
||||
researcher.get("display_name").and_then(Value::as_str),
|
||||
Some("Researcher")
|
||||
);
|
||||
assert!(researcher
|
||||
.get("when_to_use")
|
||||
.and_then(Value::as_str)
|
||||
.is_some());
|
||||
assert!(researcher.get("system_prompt").is_none());
|
||||
assert!(researcher.get("tools").is_some());
|
||||
assert!(researcher
|
||||
.get("direct_tool_count")
|
||||
.and_then(Value::as_u64)
|
||||
.is_some());
|
||||
assert!(researcher
|
||||
.get("can_run_as_user_facing_worker")
|
||||
.and_then(Value::as_bool)
|
||||
.is_some());
|
||||
|
||||
let missing = post_json_rpc(
|
||||
&rpc_base,
|
||||
2862_10,
|
||||
|
||||
Reference in New Issue
Block a user