feat(agent): expose agent library surface (#3383)

This commit is contained in:
Steven Enamakel
2026-06-04 16:25:31 -04:00
committed by GitHub
parent f514bece86
commit 1083c3112d
28 changed files with 1252 additions and 6 deletions
@@ -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(
+27
View File
@@ -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.',
+27
View File
@@ -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.',
+27
View File
@@ -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.',
+27
View File
@@ -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.',
+27
View File
@@ -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.',
+27
View File
@@ -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.',
+27
View File
@@ -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.',
+27
View File
@@ -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.',
+27
View File
@@ -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 lagente 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.',
+27
View File
@@ -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.',
+27
View File
@@ -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.',
+27
View File
@@ -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.',
+27
View File
@@ -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.',
+26
View File
@@ -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([]);
});
});
+43
View File
@@ -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;
},
};
+10
View File
@@ -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",
+1
View File
@@ -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",
+7
View File
@@ -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};
+195
View File
@@ -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());
}
}
+37
View File
@@ -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,
}
+1
View File
@@ -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;
+7 -6
View File
@@ -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 }))
})
}
+35
View File
@@ -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,