diff --git a/sdk/js/src/index.ts b/sdk/js/src/index.ts index bab7a80..a866ec1 100644 --- a/sdk/js/src/index.ts +++ b/sdk/js/src/index.ts @@ -1,6 +1,5 @@ export const KESTREL_SDK_NAME = '@evalops/kestrel-sdk' -export const DEFAULT_EVALOPS_LLM_GATEWAY_URL = - 'http://llm-gateway-service.evalops.svc.cluster.local:8080/v1' +export const DEFAULT_EVALOPS_LLM_GATEWAY_URL = 'https://llm-gateway.evalops.dev/v1' export interface EvalOpsServiceEndpoints { identityBaseUrl?: string @@ -8,6 +7,7 @@ export interface EvalOpsServiceEndpoints { skillsBaseUrl?: string memoryBaseUrl?: string tracesBaseUrl?: string + approvalsBaseUrl?: string llmGatewayBaseUrl?: string } diff --git a/src/main/evalops/config.ts b/src/main/evalops/config.ts index b66588a..f7e7115 100644 --- a/src/main/evalops/config.ts +++ b/src/main/evalops/config.ts @@ -4,6 +4,7 @@ import { EVALOPS_DEFAULT_LLM_GATEWAY_BASE_URL, EVALOPS_DEFAULT_AGENT_ID, EVALOPS_DEFAULT_AGENT_REGISTRY_BASE_URL, + EVALOPS_DEFAULT_APPROVALS_BASE_URL, EVALOPS_DEFAULT_MEMORY_BASE_URL, EVALOPS_DEFAULT_PROVIDER_REF, EVALOPS_DEFAULT_RESOURCE, @@ -23,6 +24,7 @@ export interface EvalOpsConfig { token?: string llmGatewayBaseUrl: string agentRegistryBaseUrl: string + approvalsBaseUrl: string skillsBaseUrl: string memoryBaseUrl: string tracesBaseUrl: string @@ -46,6 +48,7 @@ interface StoredEvalOpsConfig { token?: unknown llmGatewayBaseUrl?: unknown agentRegistryBaseUrl?: unknown + approvalsBaseUrl?: unknown skillsBaseUrl?: unknown memoryBaseUrl?: unknown tracesBaseUrl?: unknown @@ -86,6 +89,11 @@ export function getEvalOpsConfig(overrides: Partial = {}): EvalOp asString(stored.agentRegistryBaseUrl), EVALOPS_DEFAULT_AGENT_REGISTRY_BASE_URL ), + approvalsBaseUrl: cleanUrl( + process.env.EVALOPS_APPROVALS_BASE_URL, + asString(stored.approvalsBaseUrl), + EVALOPS_DEFAULT_APPROVALS_BASE_URL + ), skillsBaseUrl: cleanUrl( process.env.EVALOPS_SKILLS_BASE_URL, asString(stored.skillsBaseUrl), diff --git a/src/main/evalops/consumer-sdk/clients.ts b/src/main/evalops/consumer-sdk/clients.ts index c98beaa..2527edf 100644 --- a/src/main/evalops/consumer-sdk/clients.ts +++ b/src/main/evalops/consumer-sdk/clients.ts @@ -3,6 +3,7 @@ import type { AgentRegistryListRequest, AgentRegistryListResponse, AgentRegistryRecord, + AgentRegistryRegisterRequest, ApprovalGetRequest, ApprovalGetResponse, ApprovalListPendingRequest, @@ -171,28 +172,55 @@ export class AgentRegistryClient { return this.transport.request({ service: 'agent-registry', operation: 'list', - path: '/agent-registry.v1.AgentRegistryService/ListAgents', - body: request, + path: '/agents.v1.AgentService/List', + body: normalizeAgentListRequest(request), signal: options?.signal, - fallback: (reason) => ({ agents: [], ...offline(reason) }) + fallback: (reason) => ({ agents: [], total: 0, ...offline(reason) }) }) } register( - agent: AgentRegistryRecord, + agent: AgentRegistryRegisterRequest, options?: { signal?: AbortSignal } ): Promise<{ agent?: AgentRegistryRecord; offline?: boolean; reason?: string }> { return this.transport.request({ service: 'agent-registry', operation: 'register', - path: '/agent-registry.v1.AgentRegistryService/RegisterAgent', - body: agent, + path: '/agents.v1.AgentService/Register', + body: { + workspaceId: agent.workspaceId, + name: agent.name, + description: agent.description, + agentType: agent.agentType, + capabilities: agent.capabilities, + surfaces: agent.surfaces, + ownerId: agent.ownerId + }, signal: options?.signal, fallback: (reason) => ({ ...offline(reason) }) }) } } +function normalizeAgentListRequest(request: AgentRegistryListRequest): Record { + return { + workspaceId: request.workspaceId, + agentType: request.agentType, + capability: request.capability, + surface: request.surface, + status: normalizeAgentStatus(request.status), + limit: request.limit, + offset: request.offset + } +} + +function normalizeAgentStatus(status: string | undefined): string | undefined { + if (!status) return undefined + const normalized = status.trim().toUpperCase().replace(/[-\s]+/gu, '_') + if (!normalized) return undefined + return normalized.startsWith('AGENT_STATUS_') ? normalized : `AGENT_STATUS_${normalized}` +} + export class SkillsClient { constructor(private readonly transport: EvalOpsTransport) {} diff --git a/src/main/evalops/consumer-sdk/http.ts b/src/main/evalops/consumer-sdk/http.ts index 232cc94..824d69c 100644 --- a/src/main/evalops/consumer-sdk/http.ts +++ b/src/main/evalops/consumer-sdk/http.ts @@ -13,6 +13,7 @@ export interface EvalOpsTransportOptions { path: string method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' body?: unknown + query?: Record signal?: AbortSignal fallback?: (reason: string) => TFallback } @@ -69,6 +70,7 @@ function toError(value: unknown): Error { export class EvalOpsTransport { readonly baseUrl: string private readonly token?: string + private readonly serviceBaseUrls: Partial> private readonly headersOverride: Record private readonly featureFlags?: FeatureFlags private readonly offlineFallback: boolean @@ -80,6 +82,12 @@ export class EvalOpsTransport { config.baseUrl ?? getEnvValue('EVALOPS_BASE_URL') ?? DEFAULT_BASE_URL ) this.token = config.token ?? getEnvValue('EVALOPS_TOKEN') + this.serviceBaseUrls = Object.fromEntries( + Object.entries(config.serviceBaseUrls ?? {}) + .map(([service, baseUrl]) => [service, baseUrl?.trim()]) + .filter((entry): entry is [EvalOpsServiceName, string] => Boolean(entry[1])) + .map(([service, baseUrl]) => [service, trimTrailingSlash(baseUrl)]) + ) this.headersOverride = config.headers ?? {} this.featureFlags = config.featureFlags this.offlineFallback = config.offlineFallback ?? false @@ -116,6 +124,16 @@ export class EvalOpsTransport { } } + private requestUrl(options: EvalOpsTransportOptions): string { + const baseUrl = this.serviceBaseUrls[options.service] ?? this.baseUrl + const url = new URL(`${baseUrl}${options.path}`) + for (const [key, value] of Object.entries(options.query ?? {})) { + if (value === undefined || value === null || value === '') continue + url.searchParams.set(key, String(value)) + } + return url.toString() + } + async request( options: EvalOpsTransportOptions ): Promise { @@ -123,7 +141,7 @@ export class EvalOpsTransport { const method = options.method ?? 'POST' try { - const response = await this.fetchImpl(`${this.baseUrl}${options.path}`, { + const response = await this.fetchImpl(this.requestUrl(options), { method, headers: this.headers(), signal: options.signal, diff --git a/src/main/evalops/consumer-sdk/types.ts b/src/main/evalops/consumer-sdk/types.ts index b137e4b..d204d91 100644 --- a/src/main/evalops/consumer-sdk/types.ts +++ b/src/main/evalops/consumer-sdk/types.ts @@ -21,6 +21,7 @@ export type FeatureFlags = Record export interface EvalOpsClientConfig { baseUrl?: string + serviceBaseUrls?: Partial> token?: string headers?: Record featureFlags?: FeatureFlags @@ -47,13 +48,19 @@ export interface OfflineFallbackMarker { export interface AgentRegistryRecord extends JsonObject { id?: string + workspace_id?: string workspaceId?: string + organization_id?: string + organizationId?: string name?: string description?: string + agent_type?: string agentType?: string capabilities?: string[] + surface?: string surfaces?: string[] status?: string + metadata?: Record activeConfigVersion?: number ownerId?: string version?: string @@ -70,6 +77,16 @@ export interface AgentRegistryListRequest extends JsonObject { offset?: number } +export interface AgentRegistryRegisterRequest extends JsonObject { + workspaceId?: string + name: string + description?: string + agentType: string + capabilities: string[] + surfaces: string[] + ownerId?: string +} + export interface AgentRegistryListResponse extends JsonObject { agents: AgentRegistryRecord[] total?: number diff --git a/src/main/evalops/consumer.ts b/src/main/evalops/consumer.ts index 8e9bd1a..ee5bd19 100644 --- a/src/main/evalops/consumer.ts +++ b/src/main/evalops/consumer.ts @@ -8,6 +8,13 @@ export async function getEvalOpsConsumerClient(): Promise { const session = getStoredEvalOpsSession() return new EvalOpsClient({ baseUrl: config.baseUrl, + serviceBaseUrls: { + 'agent-registry': config.agentRegistryBaseUrl, + approvals: config.approvalsBaseUrl, + memory: config.memoryBaseUrl, + skills: config.skillsBaseUrl, + traces: config.tracesBaseUrl + }, token, headers: cleanHeaders({ 'X-Organization-ID': session?.organizationId, diff --git a/src/main/evalops/handlers.ts b/src/main/evalops/handlers.ts index 430aa0e..c2da25e 100644 --- a/src/main/evalops/handlers.ts +++ b/src/main/evalops/handlers.ts @@ -28,7 +28,7 @@ export function registerEvalOpsHandlers(): void { ipcMain.handle('evalops:authStatus', async () => getEvalOpsAuthStatus()) ipcMain.handle('evalops:login', async (_event, options?: EvalOpsLoginOptions) => { const status = await loginEvalOps(options) - if (status.authenticated) registerKestrelAgentInBackground('login') + if (status.authenticated) registerKestrelAgentInBackground() return status }) ipcMain.handle('evalops:logout', async () => logoutEvalOps()) diff --git a/src/main/evalops/registration.ts b/src/main/evalops/registration.ts index 5f006cc..dd7fc5c 100644 --- a/src/main/evalops/registration.ts +++ b/src/main/evalops/registration.ts @@ -1,22 +1,22 @@ -import { APP_ID, APP_NAME, APP_VERSION } from '../../shared/config' +import { APP_NAME } from '../../shared/config' import { getEvalOpsAuthStatus, getStoredEvalOpsSession } from './auth' import { getEvalOpsConfig } from './config' import { getEvalOpsConsumerClient } from './consumer' const KESTREL_CAPABILITIES = [ - 'context.capture', - 'llm.chat', - 'mcp.client', - 'meeting.recording', - 'memory.recall', - 'memory.store', - 'trace.ingest', - 'approval.request' + 'x-kestrel:context.capture', + 'responses:create', + 'mcp', + 'x-kestrel:meeting.recording', + 'x-kestrel:memory.recall', + 'x-kestrel:memory.store', + 'x-kestrel:trace.ingest', + 'x-kestrel:approval.request' ] const KESTREL_SURFACES = ['kestrel', 'desktop', 'chat', 'mcp', 'meetings'] -export async function registerKestrelAgent(reason: 'startup' | 'login' = 'startup'): Promise { +export async function registerKestrelAgent(): Promise { const status = await getEvalOpsAuthStatus() if (!status.authenticated && !status.tokenConfigured) return @@ -25,25 +25,13 @@ export async function registerKestrelAgent(reason: 'startup' | 'login' = 'startu const client = await getEvalOpsConsumerClient() const response = await client.agentRegistry.register({ - id: config.agentId, workspaceId: config.workspaceId, name: `${APP_NAME} Desktop`, description: 'Context-aware AI desktop assistant for macOS.', - agentType: 'desktop-assistant', + agentType: 'desktop', capabilities: KESTREL_CAPABILITIES, surfaces: KESTREL_SURFACES, - status: 'active', - version: APP_VERSION, - ownerId: session?.organizationId, - labels: cleanLabels({ - app_id: APP_ID, - app_name: APP_NAME, - platform: process.platform, - runtime: 'electron', - registration_reason: reason, - organization_id: session?.organizationId, - workspace_id: config.workspaceId - }) + ownerId: session?.organizationId }) if (response.offline) { @@ -51,16 +39,8 @@ export async function registerKestrelAgent(reason: 'startup' | 'login' = 'startu } } -export function registerKestrelAgentInBackground(reason: 'startup' | 'login' = 'startup'): void { - registerKestrelAgent(reason).catch((err) => { +export function registerKestrelAgentInBackground(): void { + registerKestrelAgent().catch((err) => { console.warn('[evalops:registration] Agent registration failed:', err) }) } - -function cleanLabels(labels: Record): Record { - const result: Record = {} - for (const [key, value] of Object.entries(labels)) { - if (value) result[key] = value - } - return result -} diff --git a/src/main/evalops/services.ts b/src/main/evalops/services.ts index ca8d91f..c398f77 100644 --- a/src/main/evalops/services.ts +++ b/src/main/evalops/services.ts @@ -136,11 +136,11 @@ export async function getEvalOpsServicesStatus(): Promise Promise }> = [ - { service: 'agent-registry', baseUrl: config.baseUrl, run: () => listEvalOpsAgents({ limit: 1 }) }, - { service: 'skills', baseUrl: config.baseUrl, run: () => listEvalOpsSkills({ limit: 1 }) }, - { service: 'memory', baseUrl: config.baseUrl, run: () => recallEvalOpsMemory({ query: 'kestrel', topK: 1 }) }, - { service: 'approvals', baseUrl: config.baseUrl, run: () => listEvalOpsApprovals({ limit: 1 }) }, - { service: 'traces', baseUrl: config.baseUrl, run: () => listEvalOpsTraces({ limit: 1 }) } + { service: 'agent-registry', baseUrl: config.agentRegistryBaseUrl, run: () => listEvalOpsAgents({ limit: 1 }) }, + { service: 'skills', baseUrl: config.skillsBaseUrl, run: () => listEvalOpsSkills({ limit: 1 }) }, + { service: 'memory', baseUrl: config.memoryBaseUrl, run: () => recallEvalOpsMemory({ query: 'kestrel', topK: 1 }) }, + { service: 'approvals', baseUrl: config.approvalsBaseUrl, run: () => listEvalOpsApprovals({ limit: 1 }) }, + { service: 'traces', baseUrl: config.tracesBaseUrl, run: () => listEvalOpsTraces({ limit: 1 }) } ] return Promise.all(checks.map(async (check) => { diff --git a/src/main/index.ts b/src/main/index.ts index 8e4070e..b0fa2bc 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -107,7 +107,7 @@ if (!gotSingleInstanceLock) { registerMCPHandlers(mcpManager) registerPermissionHandlers() registerEvalOpsHandlers() - registerKestrelAgentInBackground('startup') + registerKestrelAgentInBackground() registerUpdateHandlers() // Context IPC handlers diff --git a/src/renderer/main/index.html b/src/renderer/main/index.html index e4307fc..fef1ae7 100644 --- a/src/renderer/main/index.html +++ b/src/renderer/main/index.html @@ -5,7 +5,7 @@ Kestrel diff --git a/src/renderer/main/src/components/settings/EvalOpsSettings.tsx b/src/renderer/main/src/components/settings/EvalOpsSettings.tsx index b9e3465..88d8207 100644 --- a/src/renderer/main/src/components/settings/EvalOpsSettings.tsx +++ b/src/renderer/main/src/components/settings/EvalOpsSettings.tsx @@ -2,12 +2,17 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { AlertCircle, Check, LogIn, LogOut, RefreshCw, Save } from 'lucide-react' import { EVALOPS_DEFAULT_AGENT_ID, + EVALOPS_DEFAULT_AGENT_REGISTRY_BASE_URL, + EVALOPS_DEFAULT_APPROVALS_BASE_URL, EVALOPS_DEFAULT_BASE_URL, EVALOPS_DEFAULT_IDENTITY_BASE_URL, EVALOPS_DEFAULT_LLM_GATEWAY_BASE_URL, + EVALOPS_DEFAULT_MEMORY_BASE_URL, EVALOPS_DEFAULT_PROVIDER_REF, EVALOPS_DEFAULT_RESOURCE, EVALOPS_DEFAULT_SCOPES, + EVALOPS_DEFAULT_SKILLS_BASE_URL, + EVALOPS_DEFAULT_TRACES_BASE_URL, EVALOPS_DEFAULT_WORKSPACE_ID } from '@shared/config' import type { EvalOpsAuthStatus, EvalOpsServiceStatus } from '@shared/ipc' @@ -20,6 +25,7 @@ interface StoredEvalOpsConfig { resource?: string scopes?: string[] agentRegistryBaseUrl?: string + approvalsBaseUrl?: string skillsBaseUrl?: string memoryBaseUrl?: string tracesBaseUrl?: string @@ -38,6 +44,11 @@ export function EvalOpsSettings() { const [identityBaseUrl, setIdentityBaseUrl] = useState(EVALOPS_DEFAULT_IDENTITY_BASE_URL) const [baseUrl, setBaseUrl] = useState(EVALOPS_DEFAULT_BASE_URL) const [llmGatewayBaseUrl, setLlmGatewayBaseUrl] = useState(EVALOPS_DEFAULT_LLM_GATEWAY_BASE_URL) + const [agentRegistryBaseUrl, setAgentRegistryBaseUrl] = useState(EVALOPS_DEFAULT_AGENT_REGISTRY_BASE_URL) + const [approvalsBaseUrl, setApprovalsBaseUrl] = useState(EVALOPS_DEFAULT_APPROVALS_BASE_URL) + const [skillsBaseUrl, setSkillsBaseUrl] = useState(EVALOPS_DEFAULT_SKILLS_BASE_URL) + const [memoryBaseUrl, setMemoryBaseUrl] = useState(EVALOPS_DEFAULT_MEMORY_BASE_URL) + const [tracesBaseUrl, setTracesBaseUrl] = useState(EVALOPS_DEFAULT_TRACES_BASE_URL) const [token, setToken] = useState('') const [resource, setResource] = useState(EVALOPS_DEFAULT_RESOURCE) const [scopes, setScopes] = useState(EVALOPS_DEFAULT_SCOPES.join(' ')) @@ -60,6 +71,11 @@ export function EvalOpsSettings() { if (stored?.identityBaseUrl) setIdentityBaseUrl(stored.identityBaseUrl) if (stored?.baseUrl) setBaseUrl(stored.baseUrl) if (stored?.llmGatewayBaseUrl) setLlmGatewayBaseUrl(stored.llmGatewayBaseUrl) + if (stored?.agentRegistryBaseUrl) setAgentRegistryBaseUrl(stored.agentRegistryBaseUrl) + if (stored?.approvalsBaseUrl) setApprovalsBaseUrl(stored.approvalsBaseUrl) + if (stored?.skillsBaseUrl) setSkillsBaseUrl(stored.skillsBaseUrl) + if (stored?.memoryBaseUrl) setMemoryBaseUrl(stored.memoryBaseUrl) + if (stored?.tracesBaseUrl) setTracesBaseUrl(stored.tracesBaseUrl) if (stored?.token) setToken(stored.token) if (stored?.resource) setResource(stored.resource) if (stored?.scopes?.length) setScopes(stored.scopes.join(' ')) @@ -84,6 +100,11 @@ export function EvalOpsSettings() { identityBaseUrl: identityBaseUrl.trim(), baseUrl: baseUrl.trim(), llmGatewayBaseUrl: llmGatewayBaseUrl.trim(), + agentRegistryBaseUrl: agentRegistryBaseUrl.trim(), + approvalsBaseUrl: approvalsBaseUrl.trim(), + skillsBaseUrl: skillsBaseUrl.trim(), + memoryBaseUrl: memoryBaseUrl.trim(), + tracesBaseUrl: tracesBaseUrl.trim(), token: token.trim(), resource: resource.trim(), scopes: parsedScopes, @@ -104,7 +125,25 @@ export function EvalOpsSettings() { } finally { setBusy(false) } - }, [agentId, baseUrl, identityBaseUrl, llmGatewayBaseUrl, parsedScopes, provider, providerCredentialName, providerEnvironment, providerTeamId, resource, scopes, token, workspaceId]) + }, [ + agentId, + agentRegistryBaseUrl, + approvalsBaseUrl, + baseUrl, + identityBaseUrl, + llmGatewayBaseUrl, + memoryBaseUrl, + parsedScopes, + provider, + providerCredentialName, + providerEnvironment, + providerTeamId, + resource, + skillsBaseUrl, + token, + tracesBaseUrl, + workspaceId + ]) const signIn = useCallback(async () => { setBusy(true) @@ -230,6 +269,39 @@ export function EvalOpsSettings() { placeholder={EVALOPS_DEFAULT_LLM_GATEWAY_BASE_URL} /> +
+ + + + + +
+

Platform Services

- Agent registry, skills, memory, approvals, and traces are checked through the unified EvalOps platform API. + Agent registry, approvals, skills, memory, and traces are checked through their configured EvalOps service URLs.