Skip to content
Closed
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
2 changes: 1 addition & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"prepack": "npm run build",
"typecheck": "tsc --noEmit",
"test": "jest",
"test:core": "jest --runInBand --forceExit src/agent/communication/__tests__/agentMessageBus.test.ts src/agent/core/executors/__tests__/strategyExecutor.test.ts src/agent/core/executors/__tests__/hypothesisExecutor.test.ts src/agent/context/__tests__/enhancedSessionContext.test.ts src/tests/adbTools.test.ts src/services/__tests__/sessionLogger.test.ts src/services/__tests__/traceAnalysisSkillConfig.test.ts src/agent/agents/domain/__tests__/registry.test.ts src/agentv3/__tests__/sqlIncludeInjector.test.ts src/agentv3/__tests__/claudeRuntimeRuntimeSnapshots.test.ts src/middleware/__tests__/auth.test.ts src/services/__tests__/rbac.test.ts src/routes/__tests__/agentRoutesRbac.test.ts src/routes/__tests__/ownerGuardRoutes.test.ts src/routes/__tests__/requestContextRouteCoverage.test.ts src/middleware/__tests__/legacyApiCompatibility.test.ts src/services/__tests__/enterpriseDb.test.ts src/services/__tests__/enterpriseSchema.test.ts src/services/__tests__/enterpriseRepository.test.ts src/services/__tests__/runtimeSnapshotStore.test.ts src/routes/__tests__/enterpriseTraceMetadataRoutes.test.ts src/routes/__tests__/enterpriseReportRoutes.test.ts",
"test:core": "jest --runInBand --forceExit src/agent/communication/__tests__/agentMessageBus.test.ts src/agent/core/executors/__tests__/strategyExecutor.test.ts src/agent/core/executors/__tests__/hypothesisExecutor.test.ts src/agent/context/__tests__/enhancedSessionContext.test.ts src/tests/adbTools.test.ts src/services/__tests__/sessionLogger.test.ts src/services/__tests__/traceAnalysisSkillConfig.test.ts src/agent/agents/domain/__tests__/registry.test.ts src/agentv3/__tests__/sqlIncludeInjector.test.ts src/agentv3/__tests__/claudeRuntimeRuntimeSnapshots.test.ts src/middleware/__tests__/auth.test.ts src/services/__tests__/rbac.test.ts src/routes/__tests__/agentRoutesRbac.test.ts src/routes/__tests__/ownerGuardRoutes.test.ts src/routes/__tests__/requestContextRouteCoverage.test.ts src/middleware/__tests__/legacyApiCompatibility.test.ts src/services/__tests__/enterpriseDb.test.ts src/services/__tests__/enterpriseSchema.test.ts src/services/__tests__/enterpriseRepository.test.ts src/services/__tests__/runtimeSnapshotStore.test.ts src/services/providerManager/__tests__/enterpriseProviderStore.test.ts src/routes/__tests__/enterpriseTraceMetadataRoutes.test.ts src/routes/__tests__/enterpriseReportRoutes.test.ts",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:unit": "jest --testPathPatterns=src/tests",
Expand Down
25 changes: 14 additions & 11 deletions backend/src/agentOpenAI/openAiConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// This file is part of SmartPerfetto. See LICENSE for details.

import { DEFAULT_OUTPUT_LANGUAGE, outputLanguageDisplayName, parseOutputLanguage, type OutputLanguage } from '../agentv3/outputLanguage';
import { getProviderService, type OpenAIProtocol } from '../services/providerManager';
import { getProviderService, type OpenAIProtocol, type ProviderScope } from '../services/providerManager';
import { mergeIsolatedProviderEnv } from '../services/providerManager/envIsolation';

export interface OpenAIAgentConfig {
Expand Down Expand Up @@ -38,13 +38,16 @@ function parseProtocol(value: string | undefined): OpenAIProtocol {
return value === 'chat_completions' ? 'chat_completions' : 'responses';
}

export function createOpenAIEnv(providerId?: string | null): Record<string, string | undefined> {
export function createOpenAIEnv(
providerId?: string | null,
providerScope?: ProviderScope,
): Record<string, string | undefined> {
const svc = getProviderService();
const provider = typeof providerId === 'string'
? svc.getRawProvider(providerId)
? svc.getRawProvider(providerId, providerScope)
: providerId === null
? undefined
: svc.getRawEffectiveProvider();
: svc.getRawEffectiveProvider(providerScope);

if (typeof providerId === 'string' && !provider) {
throw new Error(`Provider not found: ${providerId}`);
Expand All @@ -56,13 +59,13 @@ export function createOpenAIEnv(providerId?: string | null): Record<string, stri
}

const providerEnv = providerRuntime === 'openai-agents-sdk' && provider
? svc.getEnvForProvider(provider.id)
? svc.getEnvForProvider(provider.id, providerScope)
: null;
return mergeIsolatedProviderEnv(process.env, providerEnv);
}

export function loadOpenAIConfig(providerId?: string | null): OpenAIAgentConfig {
const env = createOpenAIEnv(providerId);
export function loadOpenAIConfig(providerId?: string | null, providerScope?: ProviderScope): OpenAIAgentConfig {
const env = createOpenAIEnv(providerId, providerScope);
return {
model: env.OPENAI_MODEL || DEFAULT_MODEL,
lightModel: env.OPENAI_LIGHT_MODEL || DEFAULT_LIGHT_MODEL,
Expand All @@ -84,8 +87,8 @@ export function loadOpenAIConfig(providerId?: string | null): OpenAIAgentConfig
};
}

export function hasOpenAICredentials(providerId?: string | null): boolean {
const env = createOpenAIEnv(providerId);
export function hasOpenAICredentials(providerId?: string | null, providerScope?: ProviderScope): boolean {
const env = createOpenAIEnv(providerId, providerScope);
const baseUrl = env.OPENAI_BASE_URL || '';
return Boolean(
env.OPENAI_API_KEY
Expand All @@ -95,8 +98,8 @@ export function hasOpenAICredentials(providerId?: string | null): boolean {
);
}

export function getOpenAIRuntimeDiagnostics(providerId?: string | null) {
const config = loadOpenAIConfig(providerId);
export function getOpenAIRuntimeDiagnostics(providerId?: string | null, providerScope?: ProviderScope) {
const config = loadOpenAIConfig(providerId, providerScope);
const credentialSources: string[] = [];
if (config.apiKey) credentialSources.push('openai_api_key');
if (config.baseURL) credentialSources.push('openai_base_url');
Expand Down
12 changes: 11 additions & 1 deletion backend/src/agentOpenAI/openAiRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import {
import { DEFAULT_OUTPUT_LANGUAGE, localize, type OutputLanguage } from '../agentv3/outputLanguage';
import { loadOpenAIConfig, type OpenAIAgentConfig } from './openAiConfig';
import { createOpenAIToolsFromMcpDefinitions } from './openAiToolAdapter';
import type { ProviderScope } from '../services/providerManager';

interface OpenAISessionEntry {
history?: AgentInputItem[];
Expand Down Expand Up @@ -122,6 +123,15 @@ function parseJsonObject(value: unknown): Record<string, unknown> | undefined {
}
}

function providerScopeFromOptions(options: AnalysisOptions): ProviderScope | undefined {
if (!options.tenantId || !options.workspaceId) return undefined;
return {
tenantId: options.tenantId,
workspaceId: options.workspaceId,
userId: options.userId,
};
}

function summarizeToolOutput(value: unknown): string {
const text = typeof value === 'string' ? value : JSON.stringify(value);
if (!text) return '';
Expand Down Expand Up @@ -206,7 +216,7 @@ export class OpenAIRuntime extends EventEmitter implements IOrchestrator {
const startTime = Date.now();
let accumulatedAnswer = '';
let rounds = 0;
const config = loadOpenAIConfig(options.providerId);
const config = loadOpenAIConfig(options.providerId, providerScopeFromOptions(options));
const sceneType = classifyScene(query);
const quickMode = options.analysisMode === 'fast';

Expand Down
10 changes: 6 additions & 4 deletions backend/src/agentRuntime/runtimeSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import type { TraceProcessorService } from '../services/traceProcessorService';
import type { IOrchestrator } from '../agent/core/orchestratorTypes';
import { createClaudeRuntime } from '../agentv3';
import { getProviderService, type AgentRuntimeKind } from '../services/providerManager';
import { getProviderService, type AgentRuntimeKind, type ProviderScope } from '../services/providerManager';

export type BackendAgentRuntimeKind = AgentRuntimeKind;

Expand All @@ -26,6 +26,7 @@ export interface CreateAgentOrchestratorInput {
*/
providerId?: string | null;
runtimeOverride?: BackendAgentRuntimeKind;
providerScope?: ProviderScope;
}

function parseRuntimeEnv(value: string | undefined): BackendAgentRuntimeKind | undefined {
Expand All @@ -41,13 +42,14 @@ function parseRuntimeEnv(value: string | undefined): BackendAgentRuntimeKind | u
export function resolveAgentRuntimeSelection(
providerId?: string | null,
runtimeOverride?: BackendAgentRuntimeKind,
providerScope?: ProviderScope,
): RuntimeSelection {
const providerSvc = getProviderService();
const provider = typeof providerId === 'string'
? providerSvc.getRawProvider(providerId)
? providerSvc.getRawProvider(providerId, providerScope)
: providerId === null || runtimeOverride
? undefined
: providerSvc.getRawEffectiveProvider();
: providerSvc.getRawEffectiveProvider(providerScope);

if (typeof providerId === 'string' && !provider) {
throw new Error(`Provider not found: ${providerId}`);
Expand Down Expand Up @@ -82,7 +84,7 @@ export function resolveAgentRuntimeSelection(
}

export function createAgentOrchestrator(input: CreateAgentOrchestratorInput): IOrchestrator {
const selection = resolveAgentRuntimeSelection(input.providerId, input.runtimeOverride);
const selection = resolveAgentRuntimeSelection(input.providerId, input.runtimeOverride, input.providerScope);
switch (selection.kind) {
case 'openai-agents-sdk': {
// Lazy import keeps the OpenAI runtime isolated from Claude-only startup
Expand Down
24 changes: 16 additions & 8 deletions backend/src/agentv3/claudeConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { SceneType } from './sceneClassifier';
import { getRegisteredScenes } from './strategyLoader';
import { DEFAULT_OUTPUT_LANGUAGE, outputLanguageDisplayName, parseOutputLanguage, type OutputLanguage } from './outputLanguage';
import { mergeIsolatedProviderEnv } from '../services/providerManager/envIsolation';
import type { ProviderScope } from '../services/providerManager';

export type EffortLevel = 'low' | 'medium' | 'high' | 'max';

Expand Down Expand Up @@ -275,16 +276,19 @@ export function getSdkBinaryOption(env: Record<string, string | undefined> = pro
* configured for the Claude Agent SDK runtime.
* Falls back to raw process.env when no provider is configured.
*/
export function createSdkEnv(sessionOverrideProviderId?: string | null): Record<string, string | undefined> {
export function createSdkEnv(
sessionOverrideProviderId?: string | null,
providerScope?: ProviderScope,
): Record<string, string | undefined> {
// Lazy import to avoid circular dependency at module load time
const { getProviderService } = require('../services/providerManager');
const svc = getProviderService();

const provider = typeof sessionOverrideProviderId === 'string'
? svc.getRawProvider(sessionOverrideProviderId)
? svc.getRawProvider(sessionOverrideProviderId, providerScope)
: sessionOverrideProviderId === null
? undefined
: svc.getRawEffectiveProvider();
: svc.getRawEffectiveProvider(providerScope);

if (typeof sessionOverrideProviderId === 'string' && !provider) {
throw new Error(`Provider not found: ${sessionOverrideProviderId}`);
Expand All @@ -296,7 +300,7 @@ export function createSdkEnv(sessionOverrideProviderId?: string | null): Record<
}

const providerEnv = providerRuntime === 'claude-agent-sdk' && provider
? svc.getEnvForProvider(provider.id)
? svc.getEnvForProvider(provider.id, providerScope)
: null;
const env = mergeIsolatedProviderEnv(process.env, providerEnv);

Expand All @@ -312,16 +316,20 @@ export function createSdkEnv(sessionOverrideProviderId?: string | null): Record<
* this reads providerManager env vars at call time when the provider matches
* the Claude Agent SDK runtime.
*/
export function resolveRuntimeConfig(baseConfig: ClaudeAgentConfig, providerId?: string | null): ClaudeAgentConfig {
export function resolveRuntimeConfig(
baseConfig: ClaudeAgentConfig,
providerId?: string | null,
providerScope?: ProviderScope,
): ClaudeAgentConfig {
// Lazy import to avoid circular dependency
const { getProviderService } = require('../services/providerManager');
const svc = getProviderService();

const provider = typeof providerId === 'string'
? svc.getRawProvider(providerId)
? svc.getRawProvider(providerId, providerScope)
: providerId === null
? undefined
: svc.getRawEffectiveProvider();
: svc.getRawEffectiveProvider(providerScope);

if (typeof providerId === 'string' && !provider) {
throw new Error(`Provider not found: ${providerId}`);
Expand All @@ -333,7 +341,7 @@ export function resolveRuntimeConfig(baseConfig: ClaudeAgentConfig, providerId?:
}

const providerEnv = providerRuntime === 'claude-agent-sdk' && provider
? svc.getEnvForProvider(provider.id)
? svc.getEnvForProvider(provider.id, providerScope)
: null;

if (!providerEnv) return baseConfig;
Expand Down
20 changes: 16 additions & 4 deletions backend/src/agentv3/claudeRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ import {
saveClaudeSessionMapToRuntimeSnapshots,
type ClaudeSessionMapRuntimeEntry,
} from '../services/runtimeSnapshotStore';
import type { ProviderScope } from '../services/providerManager';

const SESSION_MAP_FILE = path.resolve(__dirname, '../../logs/claude_session_map.json');
/** Max age for session map entries before pruning (24 hours). */
Expand Down Expand Up @@ -146,6 +147,15 @@ function loadSessionMapForCurrentMode(): Map<string, SessionMapEntry> {
return legacyMap;
}

function providerScopeFromOptions(options: AnalysisOptions): ProviderScope | undefined {
if (!options.tenantId || !options.workspaceId) return undefined;
return {
tenantId: options.tenantId,
workspaceId: options.workspaceId,
userId: options.userId,
};
}

/**
* Debounce timer for session map persistence — avoids blocking event loop on every SDK message.
* P2-1: Use a Map keyed by the Map reference to support multiple ClaudeRuntime instances.
Expand Down Expand Up @@ -494,7 +504,8 @@ export class ClaudeRuntime extends EventEmitter implements IOrchestrator {
return { apps: [], primaryApp: undefined, method: 'none' as const };
});

const runtimeConfig = resolveRuntimeConfig(this.config, options.providerId);
const providerScope = providerScopeFromOptions(options);
const runtimeConfig = resolveRuntimeConfig(this.config, options.providerId, providerScope);

let queryComplexity: QueryComplexity;
let classifierSource: 'user_explicit' | 'hard_rule' | 'ai';
Expand Down Expand Up @@ -597,7 +608,7 @@ export class ClaudeRuntime extends EventEmitter implements IOrchestrator {
effectivePrompt = `${traceSection}\n\n${effectivePrompt}`;
}

const sdkEnv = createSdkEnv(options.providerId);
const sdkEnv = createSdkEnv(options.providerId, providerScope);

const { stream, close: closeSdk } = sdkQueryWithRetry({
prompt: effectivePrompt,
Expand Down Expand Up @@ -1661,7 +1672,8 @@ export class ClaudeRuntime extends EventEmitter implements IOrchestrator {
outputLanguage: this.config.outputLanguage,
});

const quickConfig = createQuickConfig(resolveRuntimeConfig(this.config, options.providerId));
const providerScope = providerScopeFromOptions(options);
const quickConfig = createQuickConfig(resolveRuntimeConfig(this.config, options.providerId, providerScope));

const { handleMessage: bridge, getAccumulatedAnswer } = createSseBridge((update: StreamingUpdate) => {
this.emitUpdate(update);
Expand Down Expand Up @@ -1691,7 +1703,7 @@ export class ClaudeRuntime extends EventEmitter implements IOrchestrator {
if (sessionMapEntry && existingSdkSessionId && enterpriseSessionMapStoreEnabled()) {
this.persistSessionMapEntry(sessionId, traceId, sessionMapKey, sessionMapEntry, options);
}
const sdkEnv = createSdkEnv(options.providerId);
const sdkEnv = createSdkEnv(options.providerId, providerScope);

// Prepend pre-queried trace data so the AI skips basic SQL turns in fast mode
let quickPrompt = query;
Expand Down
15 changes: 10 additions & 5 deletions backend/src/assistant/application/agentAnalyzeSessionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import { createAgentOrchestrator } from '../../agentRuntime';
import { getProviderService } from '../../services/providerManager';
import { resolveProviderRuntimeSnapshot } from '../../services/providerManager/providerSnapshot';
import type { AgentRuntimeKind } from '../../services/providerManager';
import type { AgentRuntimeKind, ProviderScope } from '../../services/providerManager';
import { getTraceProcessorService } from '../../services/traceProcessorService';
import {
type EnhancedSessionContext,
Expand Down Expand Up @@ -112,6 +112,7 @@ interface PrepareAnalyzeSessionInput {
query: string;
requestedSessionId?: string;
providerId?: string | null;
providerScope?: ProviderScope;
options?: any;
}

Expand Down Expand Up @@ -155,12 +156,13 @@ export class AgentAnalyzeSessionService<TSession extends AnalyzeManagedSession>

prepareSession(input: PrepareAnalyzeSessionInput): PrepareAnalyzeSessionResult<TSession> {
const { traceId, query, requestedSessionId, options = {} } = input;
const providerScope = input.providerScope;
const explicitProviderId = input.providerId !== undefined
? input.providerId
: options.providerId as string | null | undefined;
const providerSvc = getProviderService();

if (typeof explicitProviderId === 'string' && !providerSvc.getRawProvider(explicitProviderId)) {
if (typeof explicitProviderId === 'string' && !providerSvc.getRawProvider(explicitProviderId, providerScope)) {
throw new AnalyzeSessionPreparationError(`Provider not found: ${explicitProviderId}`, {
code: 'PROVIDER_NOT_FOUND',
httpStatus: 404,
Expand All @@ -169,14 +171,14 @@ export class AgentAnalyzeSessionService<TSession extends AnalyzeManagedSession>

const activeProviderId = explicitProviderId !== undefined
? undefined
: providerSvc.getRawEffectiveProvider()?.id;
: providerSvc.getRawEffectiveProvider(providerScope)?.id;
const sessionProviderId = explicitProviderId !== undefined
? explicitProviderId
: activeProviderId ?? null;
const resolveProviderSnapshotHash = (
providerId: string | null,
runtimeOverride?: AgentRuntimeKind,
) => resolveProviderRuntimeSnapshot(providerSvc, providerId, runtimeOverride).snapshotHash;
) => resolveProviderRuntimeSnapshot(providerSvc, providerId, runtimeOverride, providerScope).snapshotHash;
const sessionProviderSnapshotHash = resolveProviderSnapshotHash(sessionProviderId);

if (requestedSessionId) {
Expand Down Expand Up @@ -222,6 +224,7 @@ export class AgentAnalyzeSessionService<TSession extends AnalyzeManagedSession>
existingSession.orchestrator = createAgentOrchestrator({
traceProcessorService: getTraceProcessorService(),
providerId: liveSessionProviderId,
providerScope,
});
existingSession.providerId = liveSessionProviderId;
existingSession.providerSnapshotHash = liveSessionProviderSnapshotHash;
Expand Down Expand Up @@ -297,7 +300,7 @@ export class AgentAnalyzeSessionService<TSession extends AnalyzeManagedSession>
stateSnapshot &&
snapshotProviderId !== explicitProviderId,
);
if (!snapshotProviderMismatch && typeof snapshotProviderId === 'string' && !providerSvc.getRawProvider(snapshotProviderId)) {
if (!snapshotProviderMismatch && typeof snapshotProviderId === 'string' && !providerSvc.getRawProvider(snapshotProviderId, providerScope)) {
throw new AnalyzeSessionPreparationError(`Provider not found: ${snapshotProviderId}`, {
code: 'PROVIDER_NOT_FOUND',
httpStatus: 404,
Expand Down Expand Up @@ -326,6 +329,7 @@ export class AgentAnalyzeSessionService<TSession extends AnalyzeManagedSession>
traceProcessorService: getTraceProcessorService(),
providerId: restoredProviderId,
runtimeOverride: restoredProviderId ? undefined : stateSnapshot?.agentRuntimeKind,
providerScope,
});

const focusSnapshot = this.sessionPersistenceService.loadFocusStore(requestedSessionId);
Expand Down Expand Up @@ -497,6 +501,7 @@ export class AgentAnalyzeSessionService<TSession extends AnalyzeManagedSession>
const orchestrator: IOrchestrator = createAgentOrchestrator({
traceProcessorService: getTraceProcessorService(),
providerId: sessionProviderId,
providerScope,
});

const logger = this.createSessionLogger(sessionId);
Expand Down
Loading