Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions sdk/js/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
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'
Comment thread
cursor[bot] marked this conversation as resolved.
export const DEFAULT_EVALOPS_LLM_GATEWAY_URL = 'https://llm-gateway.evalops.dev/v1'

export interface EvalOpsServiceEndpoints {
identityBaseUrl?: string
agentRegistryBaseUrl?: string
skillsBaseUrl?: string
memoryBaseUrl?: string
tracesBaseUrl?: string
approvalsBaseUrl?: string
llmGatewayBaseUrl?: string
}

Expand Down
8 changes: 8 additions & 0 deletions src/main/evalops/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -23,6 +24,7 @@ export interface EvalOpsConfig {
token?: string
llmGatewayBaseUrl: string
agentRegistryBaseUrl: string
approvalsBaseUrl: string
skillsBaseUrl: string
memoryBaseUrl: string
tracesBaseUrl: string
Expand All @@ -46,6 +48,7 @@ interface StoredEvalOpsConfig {
token?: unknown
llmGatewayBaseUrl?: unknown
agentRegistryBaseUrl?: unknown
approvalsBaseUrl?: unknown
skillsBaseUrl?: unknown
memoryBaseUrl?: unknown
tracesBaseUrl?: unknown
Expand Down Expand Up @@ -86,6 +89,11 @@ export function getEvalOpsConfig(overrides: Partial<EvalOpsConfig> = {}): 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),
Expand Down
40 changes: 34 additions & 6 deletions src/main/evalops/consumer-sdk/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
AgentRegistryListRequest,
AgentRegistryListResponse,
AgentRegistryRecord,
AgentRegistryRegisterRequest,
ApprovalGetRequest,
ApprovalGetResponse,
ApprovalListPendingRequest,
Expand Down Expand Up @@ -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<string, unknown> {
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) {}

Expand Down
20 changes: 19 additions & 1 deletion src/main/evalops/consumer-sdk/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface EvalOpsTransportOptions<TFallback = unknown> {
path: string
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
body?: unknown
query?: Record<string, string | number | boolean | null | undefined>
signal?: AbortSignal
fallback?: (reason: string) => TFallback
}
Expand Down Expand Up @@ -69,6 +70,7 @@ function toError(value: unknown): Error {
export class EvalOpsTransport {
readonly baseUrl: string
private readonly token?: string
private readonly serviceBaseUrls: Partial<Record<EvalOpsServiceName, string>>
private readonly headersOverride: Record<string, string>
private readonly featureFlags?: FeatureFlags
private readonly offlineFallback: boolean
Expand All @@ -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
Expand Down Expand Up @@ -116,14 +124,24 @@ export class EvalOpsTransport {
}
}

private requestUrl(options: EvalOpsTransportOptions<unknown>): 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<TResponse>(
options: EvalOpsTransportOptions<TResponse>
): Promise<TResponse> {
this.metrics.requests += 1
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,
Expand Down
17 changes: 17 additions & 0 deletions src/main/evalops/consumer-sdk/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type FeatureFlags = Record<string, FeatureFlagValue>

export interface EvalOpsClientConfig {
baseUrl?: string
serviceBaseUrls?: Partial<Record<EvalOpsServiceName, string>>
token?: string
headers?: Record<string, string>
featureFlags?: FeatureFlags
Expand All @@ -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<string, string>
activeConfigVersion?: number
ownerId?: string
version?: string
Expand All @@ -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
Expand Down
7 changes: 7 additions & 0 deletions src/main/evalops/consumer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ export async function getEvalOpsConsumerClient(): Promise<EvalOpsClient> {
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,
Expand Down
2 changes: 1 addition & 1 deletion src/main/evalops/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
48 changes: 14 additions & 34 deletions src/main/evalops/registration.ts
Original file line number Diff line number Diff line change
@@ -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'
Comment thread
cursor[bot] marked this conversation as resolved.
]

const KESTREL_SURFACES = ['kestrel', 'desktop', 'chat', 'mcp', 'meetings']

export async function registerKestrelAgent(reason: 'startup' | 'login' = 'startup'): Promise<void> {
export async function registerKestrelAgent(): Promise<void> {
const status = await getEvalOpsAuthStatus()
if (!status.authenticated && !status.tokenConfigured) return

Expand All @@ -25,42 +25,22 @@ 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) {
console.warn('[evalops:registration] Agent registration used offline fallback:', response.reason)
}
}

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<string, string | undefined>): Record<string, string> {
const result: Record<string, string> = {}
for (const [key, value] of Object.entries(labels)) {
if (value) result[key] = value
}
return result
}
10 changes: 5 additions & 5 deletions src/main/evalops/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,11 @@ export async function getEvalOpsServicesStatus(): Promise<EvalOpsServiceStatus[]
baseUrl: string
run: () => Promise<unknown>
}> = [
{ 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) => {
Expand Down
2 changes: 1 addition & 1 deletion src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ if (!gotSingleInstanceLock) {
registerMCPHandlers(mcpManager)
registerPermissionHandlers()
registerEvalOpsHandlers()
registerKestrelAgentInBackground('startup')
registerKestrelAgentInBackground()
registerUpdateHandlers()

// Context IPC handlers
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/main/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' http://*.evalops.svc.cluster.local:* https://api.openai.com"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https://*.evalops.dev https://api.openai.com"
/>
<title>Kestrel</title>
</head>
Expand Down
Loading
Loading