diff --git a/app/src/components/intelligence/AgentsLibraryPanel.test.tsx b/app/src/components/intelligence/AgentsLibraryPanel.test.tsx new file mode 100644 index 000000000..c20fabcf8 --- /dev/null +++ b/app/src/components/intelligence/AgentsLibraryPanel.test.tsx @@ -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 { + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + }); +}); diff --git a/app/src/components/intelligence/AgentsLibraryPanel.tsx b/app/src/components/intelligence/AgentsLibraryPanel.tsx new file mode 100644 index 000000000..3130672ec --- /dev/null +++ b/app/src/components/intelligence/AgentsLibraryPanel.tsx @@ -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; + 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([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [drafts, setDrafts] = useState>({}); + const [copiedId, setCopiedId] = useState(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 ( +
+
+
+

+ + {t('intelligence.agents.title')} +

+

+ {t('intelligence.agents.subtitle')} +

+
+ +
+ + {loading && ( +
+
+ {t('intelligence.agents.loading')} +
+ )} + + {error && !loading && ( +
+ {t('intelligence.agents.failedToLoad')}: {error} +
+ )} + + {!loading && !error && visibleAgents.length === 0 && ( +
+ {t('intelligence.agents.empty')} +
+ )} + + {!loading && !error && visibleAgents.length > 0 && ( +
    + {visibleAgents.map(agent => { + const draft = drafts[agent.id] ?? ''; + const running = runningAgentId === agent.id; + return ( +
  • +
    +
    +
    + + {agent.display_name} + + + {agent.id} + + + {modelLabel(agent, t)} + +
    +

    + {agent.when_to_use} +

    +
    + {capabilityChips(agent, t).map(chip => ( + + {chip} + + ))} +
    +
    +
    + + +
    +
    + +
    + + 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" + /> + +
    +
  • + ); + })} +
+ )} +
+ ); +} diff --git a/app/src/components/intelligence/IntelligenceTasksTab.tsx b/app/src/components/intelligence/IntelligenceTasksTab.tsx index 9122fb626..d2fa4e4ba 100644 --- a/app/src/components/intelligence/IntelligenceTasksTab.tsx +++ b/app/src/components/intelligence/IntelligenceTasksTab.tsx @@ -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(null); const [workingCardId, setWorkingCardId] = useState(null); + const [runningAgentId, setRunningAgentId] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [actionError, setActionError] = useState(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() { )} + + {/* Personal board — always present so users can manage their own tasks. */}
diff --git a/app/src/components/intelligence/__tests__/IntelligenceTasksTab.test.tsx b/app/src/components/intelligence/__tests__/IntelligenceTasksTab.test.tsx index 98c1191f3..1f83383de 100644 --- a/app/src/components/intelligence/__tests__/IntelligenceTasksTab.test.tsx +++ b/app/src/components/intelligence/__tests__/IntelligenceTasksTab.test.tsx @@ -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( diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index dc527a2b8..52b8a32ab 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -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.', diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index b3db3295a..a0fc9893c 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -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.', diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index dabe2c34f..22d4cc3da 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -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.', diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index c0334d4b3..34e4d09db 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -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.', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index 6748e3bec..0626d0e2f 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -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.', diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index 08dec245f..315a407df 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -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.', diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index 92c9e54cb..a65b3adf7 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -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.', diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index bbb8b2009..1e1db5fb5 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -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.', diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index 7ae228265..3de366429 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -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.', diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index b7171c436..e68ea653a 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -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.', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index 763f2a2be..3d34c9dbd 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -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.', diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index 8b9699e3a..1bdbaf38b 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -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.', diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index 910380db5..2f91e098a 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -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.', diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index ac899a461..36a983d9f 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -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.', diff --git a/app/src/services/api/agentLibraryApi.test.ts b/app/src/services/api/agentLibraryApi.test.ts new file mode 100644 index 000000000..a2ec9dd8e --- /dev/null +++ b/app/src/services/api/agentLibraryApi.test.ts @@ -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([]); + }); +}); diff --git a/app/src/services/api/agentLibraryApi.ts b/app/src/services/api/agentLibraryApi.ts new file mode 100644 index 000000000..339ad1fb9 --- /dev/null +++ b/app/src/services/api/agentLibraryApi.ts @@ -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 => { + 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; + }, +}; diff --git a/src/openhuman/about_app/catalog_data.rs b/src/openhuman/about_app/catalog_data.rs index 458932d4d..a249b6efc 100644 --- a/src/openhuman/about_app/catalog_data.rs +++ b/src/openhuman/about_app/catalog_data.rs @@ -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", diff --git a/src/openhuman/about_app/catalog_tests.rs b/src/openhuman/about_app/catalog_tests.rs index d7c215d87..c5aa57731 100644 --- a/src/openhuman/about_app/catalog_tests.rs +++ b/src/openhuman/about_app/catalog_tests.rs @@ -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", diff --git a/src/openhuman/agent/library/mod.rs b/src/openhuman/agent/library/mod.rs new file mode 100644 index 000000000..51de0edfb --- /dev/null +++ b/src/openhuman/agent/library/mod.rs @@ -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}; diff --git a/src/openhuman/agent/library/ops.rs b/src/openhuman/agent/library/ops.rs new file mode 100644 index 000000000..d7aabbfc6 --- /dev/null +++ b/src/openhuman/agent/library/ops.rs @@ -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, 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::>(); + 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::>(); + 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 { + 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()); + } +} diff --git a/src/openhuman/agent/library/types.rs b/src/openhuman/agent/library/types.rs new file mode 100644 index 000000000..0fe8d8134 --- /dev/null +++ b/src/openhuman/agent/library/types.rs @@ -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, +} + +#[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, + pub uses_wildcard_tools: bool, + pub subagent_ids: Vec, + 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, +} diff --git a/src/openhuman/agent/mod.rs b/src/openhuman/agent/mod.rs index 99bbe84fa..8e2e53f74 100644 --- a/src/openhuman/agent/mod.rs +++ b/src/openhuman/agent/mod.rs @@ -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; diff --git a/src/openhuman/agent/schemas.rs b/src/openhuman/agent/schemas.rs index abf415904..854f4bdd3 100644 --- a/src/openhuman/agent/schemas.rs +++ b/src/openhuman/agent/schemas.rs @@ -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 /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) -> ControllerFuture { fn handle_list_definitions(_params: Map) -> 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 })) }) } diff --git a/tests/json_rpc_e2e.rs b/tests/json_rpc_e2e.rs index 5f2ef0249..ebc8b5a56 100644 --- a/tests/json_rpc_e2e.rs +++ b/tests/json_rpc_e2e.rs @@ -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,