mirror of
https://github.com/jeffusion/gitea-ai-assistant.git
synced 2026-03-27 10:05:50 +00:00
feat(review): add triage agent for smart specialist routing
Implement TriageAgent with heuristic fast path (skip trivial changes like
lockfiles, CI configs, docs-only) and LLM fallback via chatForRole('planner').
Orchestrator now runs triage before specialist dispatch, only invoking agents
for relevant domains instead of all 4 specialists on every change.
Uses the pre-reserved 'planner' model role that was defined in DB schema and
frontend UI but never wired to backend logic.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
235
src/review/__tests__/triage-agent.test.ts
Normal file
235
src/review/__tests__/triage-agent.test.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import type { LLMChatResponse, ModelRole } from '../../llm/types';
|
||||
import { TriageAgent } from '../agents/triage-agent';
|
||||
import type { ChangedFile, FindingCategory, ReviewContext } from '../types';
|
||||
|
||||
const ALL_DOMAINS: FindingCategory[] = [
|
||||
'correctness',
|
||||
'security',
|
||||
'reliability',
|
||||
'maintainability',
|
||||
];
|
||||
|
||||
function makeChangedFile(overrides: Partial<ChangedFile> = {}): ChangedFile {
|
||||
return {
|
||||
path: 'src/file.ts',
|
||||
status: 'M',
|
||||
additions: 1,
|
||||
deletions: 1,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeContext(overrides: Partial<ReviewContext> = {}): ReviewContext {
|
||||
return {
|
||||
workspacePath: '/tmp/workspace',
|
||||
mirrorPath: '/tmp/mirror',
|
||||
diff: 'diff --git a/src/file.ts b/src/file.ts\n+const x = 1;',
|
||||
changedFiles: [makeChangedFile()],
|
||||
parsedDiff: [],
|
||||
fileContents: {},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeChatResponse(content: string | null): LLMChatResponse {
|
||||
return {
|
||||
content,
|
||||
toolCalls: [],
|
||||
finishReason: 'stop',
|
||||
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
type ChatCall = {
|
||||
role: ModelRole;
|
||||
request: any;
|
||||
};
|
||||
|
||||
function createMockGateway(
|
||||
implementation: (role: ModelRole, request: any) => Promise<LLMChatResponse>
|
||||
) {
|
||||
const calls: ChatCall[] = [];
|
||||
|
||||
return {
|
||||
gateway: {
|
||||
chatForRole: async (role: ModelRole, request: any) => {
|
||||
calls.push({ role, request });
|
||||
return implementation(role, request);
|
||||
},
|
||||
},
|
||||
getCalls: () => calls,
|
||||
};
|
||||
}
|
||||
|
||||
describe('TriageAgent', () => {
|
||||
test('heuristic: empty changedFiles -> trivial + correctness (no LLM call)', async () => {
|
||||
const { gateway, getCalls } = createMockGateway(async () =>
|
||||
makeChatResponse(JSON.stringify({ complexity: 'complex', relevant_domains: ALL_DOMAINS }))
|
||||
);
|
||||
const agent = new TriageAgent(gateway as any);
|
||||
|
||||
const result = await agent.analyze(makeContext({ changedFiles: [] }));
|
||||
|
||||
expect(result.complexity).toBe('trivial');
|
||||
expect(result.relevantDomains).toEqual(['correctness']);
|
||||
expect(getCalls()).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('heuristic: all non-code files -> trivial + correctness (no LLM call)', async () => {
|
||||
const { gateway, getCalls } = createMockGateway(async () =>
|
||||
makeChatResponse(JSON.stringify({ complexity: 'complex', relevant_domains: ALL_DOMAINS }))
|
||||
);
|
||||
const agent = new TriageAgent(gateway as any);
|
||||
|
||||
const result = await agent.analyze(
|
||||
makeContext({
|
||||
changedFiles: [
|
||||
makeChangedFile({ path: 'README.md' }),
|
||||
makeChangedFile({ path: 'config/app.json' }),
|
||||
makeChangedFile({ path: 'styles/base.css' }),
|
||||
makeChangedFile({ path: 'assets/logo.png' }),
|
||||
makeChangedFile({ path: 'bun.lock', additions: 10, deletions: 10 }),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.complexity).toBe('trivial');
|
||||
expect(result.relevantDomains).toEqual(['correctness']);
|
||||
expect(getCalls()).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('heuristic: single file <=3 line changes -> trivial + correctness (no LLM call)', async () => {
|
||||
const { gateway, getCalls } = createMockGateway(async () =>
|
||||
makeChatResponse(JSON.stringify({ complexity: 'complex', relevant_domains: ALL_DOMAINS }))
|
||||
);
|
||||
const agent = new TriageAgent(gateway as any);
|
||||
|
||||
const result = await agent.analyze(
|
||||
makeContext({
|
||||
changedFiles: [makeChangedFile({ path: 'src/app.ts', additions: 1, deletions: 2 })],
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.complexity).toBe('trivial');
|
||||
expect(result.relevantDomains).toEqual(['correctness']);
|
||||
expect(getCalls()).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('heuristic: security-sensitive small PR -> standard + correctness/security (no LLM call)', async () => {
|
||||
const { gateway, getCalls } = createMockGateway(async () =>
|
||||
makeChatResponse(JSON.stringify({ complexity: 'complex', relevant_domains: ALL_DOMAINS }))
|
||||
);
|
||||
const agent = new TriageAgent(gateway as any);
|
||||
|
||||
const result = await agent.analyze(
|
||||
makeContext({
|
||||
changedFiles: [
|
||||
makeChangedFile({ path: 'src/auth/service.ts', additions: 20, deletions: 10 }),
|
||||
makeChangedFile({ path: 'src/user/profile.ts', additions: 5, deletions: 5 }),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.complexity).toBe('standard');
|
||||
expect(result.relevantDomains).toEqual(['correctness', 'security']);
|
||||
expect(getCalls()).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('heuristic: large PR by file count (>20) -> complex + all domains (no LLM call)', async () => {
|
||||
const { gateway, getCalls } = createMockGateway(async () =>
|
||||
makeChatResponse(
|
||||
JSON.stringify({ complexity: 'standard', relevant_domains: ['correctness'] })
|
||||
)
|
||||
);
|
||||
const agent = new TriageAgent(gateway as any);
|
||||
|
||||
const changedFiles = Array.from({ length: 21 }, (_, index) =>
|
||||
makeChangedFile({ path: `src/file-${index}.ts`, additions: 2, deletions: 1 })
|
||||
);
|
||||
|
||||
const result = await agent.analyze(makeContext({ changedFiles }));
|
||||
|
||||
expect(result.complexity).toBe('complex');
|
||||
expect(result.relevantDomains).toEqual(ALL_DOMAINS);
|
||||
expect(getCalls()).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('heuristic: large PR by total changes (>500) -> complex + all domains (no LLM call)', async () => {
|
||||
const { gateway, getCalls } = createMockGateway(async () =>
|
||||
makeChatResponse(
|
||||
JSON.stringify({ complexity: 'standard', relevant_domains: ['correctness'] })
|
||||
)
|
||||
);
|
||||
const agent = new TriageAgent(gateway as any);
|
||||
|
||||
const result = await agent.analyze(
|
||||
makeContext({
|
||||
changedFiles: [
|
||||
makeChangedFile({ path: 'src/a.ts', additions: 250, deletions: 10 }),
|
||||
makeChangedFile({ path: 'src/b.ts', additions: 240, deletions: 10 }),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.complexity).toBe('complex');
|
||||
expect(result.relevantDomains).toEqual(ALL_DOMAINS);
|
||||
expect(getCalls()).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('LLM fallback: standard code change calls planner and returns parsed JSON result', async () => {
|
||||
const { gateway, getCalls } = createMockGateway(async () =>
|
||||
makeChatResponse(
|
||||
JSON.stringify({
|
||||
complexity: 'standard',
|
||||
relevant_domains: ['security', 'maintainability'],
|
||||
rationale: '跨文件业务逻辑调整',
|
||||
})
|
||||
)
|
||||
);
|
||||
const agent = new TriageAgent(gateway as any);
|
||||
|
||||
const result = await agent.analyze(
|
||||
makeContext({
|
||||
changedFiles: [
|
||||
makeChangedFile({ path: 'src/service/order.ts', additions: 10, deletions: 6 }),
|
||||
makeChangedFile({ path: 'src/controller/order.ts', additions: 12, deletions: 8 }),
|
||||
makeChangedFile({ path: 'src/repo/order.ts', additions: 8, deletions: 6 }),
|
||||
],
|
||||
diff: 'diff --git a/src/service/order.ts b/src/service/order.ts\n+export function calc(){}',
|
||||
})
|
||||
);
|
||||
|
||||
const calls = getCalls();
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0].role).toBe('planner');
|
||||
expect(calls[0].request.temperature).toBe(0);
|
||||
expect(calls[0].request.responseFormat).toBe('json');
|
||||
|
||||
expect(result.complexity).toBe('standard');
|
||||
expect(result.relevantDomains).toEqual(['correctness', 'security', 'maintainability']);
|
||||
expect(result.rationale).toBe('跨文件业务逻辑调整');
|
||||
});
|
||||
|
||||
test('LLM fallback: planner throws -> fallback standard + all domains', async () => {
|
||||
const { gateway, getCalls } = createMockGateway(async () => {
|
||||
throw new Error('planner unavailable');
|
||||
});
|
||||
const agent = new TriageAgent(gateway as any);
|
||||
|
||||
const result = await agent.analyze(
|
||||
makeContext({
|
||||
changedFiles: [
|
||||
makeChangedFile({ path: 'src/service/foo.ts', additions: 10, deletions: 4 }),
|
||||
makeChangedFile({ path: 'src/service/bar.ts', additions: 12, deletions: 6 }),
|
||||
makeChangedFile({ path: 'src/service/baz.ts', additions: 8, deletions: 10 }),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
expect(getCalls()).toHaveLength(1);
|
||||
expect(result.complexity).toBe('standard');
|
||||
expect(result.relevantDomains).toEqual(ALL_DOMAINS);
|
||||
expect(result.rationale).toContain('LLM');
|
||||
});
|
||||
});
|
||||
276
src/review/agents/triage-agent.ts
Normal file
276
src/review/agents/triage-agent.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
/**
|
||||
* Triage Agent — lightweight intent recognition using the 'planner' model role.
|
||||
*
|
||||
* Analyzes the change summary (file list + basic stats) to determine:
|
||||
* 1. Change complexity (trivial / standard / complex)
|
||||
* 2. Which specialist domains are relevant
|
||||
*
|
||||
* This avoids wasting tokens by running all 4 specialist agents on trivial changes
|
||||
* (e.g. README typo fixes, string-only edits, pure documentation changes).
|
||||
*/
|
||||
|
||||
import config from '../../config';
|
||||
import type { LLMGateway } from '../../llm/gateway';
|
||||
import type { LLMMessage } from '../../llm/types';
|
||||
import { withGlobalPrompt } from '../../utils/global-prompt';
|
||||
import { logger } from '../../utils/logger';
|
||||
import type { ChangedFile, FindingCategory, ReviewContext } from '../types';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type TriageComplexity = 'trivial' | 'standard' | 'complex';
|
||||
|
||||
export interface TriageResult {
|
||||
/** How complex the change is — drives how many agents to dispatch. */
|
||||
complexity: TriageComplexity;
|
||||
/** Which specialist domains are relevant for this change. */
|
||||
relevantDomains: FindingCategory[];
|
||||
/** Brief rationale from the planner model. */
|
||||
rationale: string;
|
||||
}
|
||||
|
||||
/** All valid finding categories. */
|
||||
const ALL_DOMAINS: FindingCategory[] = [
|
||||
'correctness',
|
||||
'security',
|
||||
'reliability',
|
||||
'maintainability',
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Triage Agent
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export class TriageAgent {
|
||||
constructor(private readonly gateway: LLMGateway) {}
|
||||
|
||||
/**
|
||||
* Analyze the review context and return a triage decision.
|
||||
* Uses the 'planner' role for a lightweight, fast LLM call.
|
||||
*
|
||||
* If the planner role is not configured or the call fails,
|
||||
* falls back to a heuristic-based triage.
|
||||
*/
|
||||
async analyze(context: ReviewContext): Promise<TriageResult> {
|
||||
// First try heuristic-based fast path (no LLM call needed for obvious cases)
|
||||
const heuristicResult = this.heuristicTriage(context.changedFiles);
|
||||
if (heuristicResult) {
|
||||
logger.info('Triage: 使用启发式规则快速分流', {
|
||||
complexity: heuristicResult.complexity,
|
||||
domains: heuristicResult.relevantDomains.join(','),
|
||||
rationale: heuristicResult.rationale,
|
||||
});
|
||||
return heuristicResult;
|
||||
}
|
||||
|
||||
// Fall back to LLM-based triage
|
||||
try {
|
||||
return await this.llmTriage(context);
|
||||
} catch (error) {
|
||||
logger.warn('Triage: LLM 调用失败,回退到启发式全量派发', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
return {
|
||||
complexity: 'standard',
|
||||
relevantDomains: [...ALL_DOMAINS],
|
||||
rationale: 'Triage LLM 调用失败,使用默认全量审查',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Heuristic-based triage — no LLM call needed.
|
||||
* Returns null if heuristic is inconclusive (should use LLM).
|
||||
*/
|
||||
private heuristicTriage(changedFiles: ChangedFile[]): TriageResult | null {
|
||||
if (changedFiles.length === 0) {
|
||||
return {
|
||||
complexity: 'trivial',
|
||||
relevantDomains: ['correctness'],
|
||||
rationale: '无变更文件',
|
||||
};
|
||||
}
|
||||
|
||||
const NON_CODE_EXTENSIONS = new Set([
|
||||
'.md',
|
||||
'.txt',
|
||||
'.rst',
|
||||
'.adoc', // docs
|
||||
'.json',
|
||||
'.yaml',
|
||||
'.yml',
|
||||
'.toml',
|
||||
'.ini', // config (non-security)
|
||||
'.css',
|
||||
'.scss',
|
||||
'.less',
|
||||
'.svg', // styles/assets
|
||||
'.png',
|
||||
'.jpg',
|
||||
'.jpeg',
|
||||
'.gif',
|
||||
'.ico',
|
||||
'.webp', // images
|
||||
'.lock', // lockfiles
|
||||
]);
|
||||
|
||||
const SECURITY_SENSITIVE_PATTERNS = [
|
||||
/auth/i,
|
||||
/login/i,
|
||||
/password/i,
|
||||
/secret/i,
|
||||
/token/i,
|
||||
/crypt/i,
|
||||
/permission/i,
|
||||
/role/i,
|
||||
/acl/i,
|
||||
/cors/i,
|
||||
/csrf/i,
|
||||
/xss/i,
|
||||
/\.env/,
|
||||
/credential/i,
|
||||
/oauth/i,
|
||||
/jwt/i,
|
||||
/session/i,
|
||||
];
|
||||
|
||||
const allNonCode = changedFiles.every((f) => {
|
||||
const ext = f.path.substring(f.path.lastIndexOf('.')).toLowerCase();
|
||||
return NON_CODE_EXTENSIONS.has(ext);
|
||||
});
|
||||
|
||||
if (allNonCode) {
|
||||
return {
|
||||
complexity: 'trivial',
|
||||
relevantDomains: ['correctness'],
|
||||
rationale: '所有变更文件均为非代码文件(文档/配置/资源)',
|
||||
};
|
||||
}
|
||||
|
||||
// Very small change (≤3 lines total) in a single file → trivial
|
||||
const totalChanges = changedFiles.reduce((sum, f) => sum + f.additions + f.deletions, 0);
|
||||
if (changedFiles.length === 1 && totalChanges <= 3) {
|
||||
return {
|
||||
complexity: 'trivial',
|
||||
relevantDomains: ['correctness'],
|
||||
rationale: `单文件微量变更(${totalChanges} 行)`,
|
||||
};
|
||||
}
|
||||
|
||||
// Check for security-sensitive files
|
||||
const hasSecurityFiles = changedFiles.some((f) =>
|
||||
SECURITY_SENSITIVE_PATTERNS.some((p) => p.test(f.path))
|
||||
);
|
||||
|
||||
// Large PR (many files or large changes) → complex
|
||||
if (changedFiles.length > 20 || totalChanges > 500) {
|
||||
return {
|
||||
complexity: 'complex',
|
||||
relevantDomains: [...ALL_DOMAINS],
|
||||
rationale: `大规模变更(${changedFiles.length} 文件, ${totalChanges} 行)`,
|
||||
};
|
||||
}
|
||||
|
||||
// Security-sensitive file detected → ensure security agent is included
|
||||
if (hasSecurityFiles && changedFiles.length <= 5 && totalChanges <= 100) {
|
||||
return {
|
||||
complexity: 'standard',
|
||||
relevantDomains: ['correctness', 'security'],
|
||||
rationale: '涉及安全相关文件,仅派发 correctness + security',
|
||||
};
|
||||
}
|
||||
|
||||
// Inconclusive — let LLM decide
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* LLM-based triage using the 'planner' role.
|
||||
*/
|
||||
private async llmTriage(context: ReviewContext): Promise<TriageResult> {
|
||||
const fileSummary = context.changedFiles
|
||||
.map((f) => `${f.status} ${f.path} (+${f.additions} -${f.deletions})`)
|
||||
.join('\n');
|
||||
|
||||
// Use a small slice of diff for context (just the first 2000 chars for speed)
|
||||
const diffPreview = context.diff.slice(0, 2000);
|
||||
|
||||
const prompt = `你是代码审查分流专家。分析以下变更并判断其复杂度和需要哪些审查领域。
|
||||
|
||||
变更文件列表:
|
||||
${fileSummary}
|
||||
|
||||
Diff 预览(前2000字符):
|
||||
${diffPreview}
|
||||
|
||||
判断标准:
|
||||
- **trivial**: 纯文档、注释、字符串修改、格式化、重命名等无逻辑变更 → 只需 correctness
|
||||
- **standard**: 单模块逻辑修改、普通功能开发 → 按实际涉及领域选择
|
||||
- **complex**: 多模块/跨层修改、架构变更、并发/安全关键路径 → 全部领域
|
||||
|
||||
可选领域:correctness(逻辑正确性), security(安全), reliability(可靠性), maintainability(可维护性)
|
||||
|
||||
返回 JSON:
|
||||
{
|
||||
"complexity": "trivial" | "standard" | "complex",
|
||||
"relevant_domains": ["correctness", ...],
|
||||
"rationale": "简要理由"
|
||||
}`;
|
||||
|
||||
const messages: LLMMessage[] = [
|
||||
{
|
||||
role: 'system',
|
||||
content: withGlobalPrompt(
|
||||
'你是代码变更分流专家,快速判断变更复杂度。返回结构化 JSON,不输出额外文字。',
|
||||
config.review.globalPrompt
|
||||
),
|
||||
},
|
||||
{ role: 'user', content: prompt },
|
||||
];
|
||||
|
||||
const response = await this.gateway.chatForRole('planner', {
|
||||
messages,
|
||||
temperature: 0,
|
||||
responseFormat: 'json',
|
||||
});
|
||||
|
||||
const content = response.content;
|
||||
if (!content) {
|
||||
throw new Error('Triage: planner 模型返回空结果');
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(content);
|
||||
|
||||
// Validate and normalize
|
||||
const complexity = (['trivial', 'standard', 'complex'] as const).includes(parsed.complexity)
|
||||
? (parsed.complexity as TriageComplexity)
|
||||
: 'standard';
|
||||
|
||||
const relevantDomains: FindingCategory[] = Array.isArray(parsed.relevant_domains)
|
||||
? (parsed.relevant_domains.filter((d: string) =>
|
||||
ALL_DOMAINS.includes(d as FindingCategory)
|
||||
) as FindingCategory[])
|
||||
: [...ALL_DOMAINS];
|
||||
|
||||
// Ensure at least correctness is always included
|
||||
if (!relevantDomains.includes('correctness')) {
|
||||
relevantDomains.unshift('correctness');
|
||||
}
|
||||
|
||||
const result: TriageResult = {
|
||||
complexity,
|
||||
relevantDomains,
|
||||
rationale: parsed.rationale || '',
|
||||
};
|
||||
|
||||
logger.info('Triage: LLM 分流完成', {
|
||||
complexity: result.complexity,
|
||||
domains: result.relevantDomains.join(','),
|
||||
rationale: result.rationale,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { logger } from '../utils/logger';
|
||||
import { DebateOrchestrator } from './agents/debate-orchestrator';
|
||||
import { JudgeAgent } from './agents/judge-agent';
|
||||
import { ReflexionAgent } from './agents/reflexion-agent';
|
||||
import { TriageAgent, type TriageResult } from './agents/triage-agent';
|
||||
import { DiffExtractor } from './context/diff-extractor';
|
||||
import { LocalRepoManager, LocalRepoPaths } from './context/local-repo-manager';
|
||||
import { LearningSystem } from './learning/learning-system';
|
||||
@@ -44,12 +45,14 @@ function summarizeGatedCount(gatedCount: number): string {
|
||||
export class ReviewOrchestrator {
|
||||
private readonly gateway: LLMGateway;
|
||||
private readonly toolRegistry: ToolRegistry;
|
||||
private readonly agentMap: Record<string, ReflexionAgent>;
|
||||
private readonly correctnessAgent: ReflexionAgent;
|
||||
private readonly securityAgent: ReflexionAgent;
|
||||
private readonly reliabilityAgent: ReflexionAgent;
|
||||
private readonly maintainabilityAgent: ReflexionAgent;
|
||||
private readonly judgeAgent: JudgeAgent;
|
||||
private readonly debateOrchestrator: DebateOrchestrator;
|
||||
private readonly triageAgent: TriageAgent;
|
||||
private readonly memoryStore?: VectorMemoryStore;
|
||||
private readonly learningSystem?: LearningSystem;
|
||||
|
||||
@@ -121,6 +124,15 @@ export class ReviewOrchestrator {
|
||||
|
||||
this.judgeAgent = new JudgeAgent();
|
||||
this.debateOrchestrator = new DebateOrchestrator(this.gateway);
|
||||
this.triageAgent = new TriageAgent(this.gateway);
|
||||
|
||||
// Build agent map for dynamic dispatch
|
||||
this.agentMap = {
|
||||
correctness: this.correctnessAgent,
|
||||
security: this.securityAgent,
|
||||
reliability: this.reliabilityAgent,
|
||||
maintainability: this.maintainabilityAgent,
|
||||
};
|
||||
}
|
||||
|
||||
async execute(run: ReviewRun): Promise<void> {
|
||||
@@ -188,6 +200,32 @@ export class ReviewOrchestrator {
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Triage: 决定哪些 specialist 需要参与 ─────────────────────────
|
||||
let triage: TriageResult | null = null;
|
||||
const enableTriage = config.review.enableTriage ?? true;
|
||||
|
||||
if (enableTriage) {
|
||||
const triageStart = Date.now();
|
||||
await this.store.addStep({
|
||||
runId: run.id,
|
||||
stepName: 'triage',
|
||||
status: 'started',
|
||||
startedAt: new Date(triageStart).toISOString(),
|
||||
});
|
||||
|
||||
triage = await this.triageAgent.analyze(context);
|
||||
|
||||
await this.store.addStep({
|
||||
runId: run.id,
|
||||
stepName: 'triage',
|
||||
status: 'succeeded',
|
||||
startedAt: new Date(triageStart).toISOString(),
|
||||
finishedAt: new Date().toISOString(),
|
||||
latencyMs: Date.now() - triageStart,
|
||||
});
|
||||
}
|
||||
|
||||
// ── 按 triage 结果选择性派发 specialists ─────────────────────────
|
||||
const agentStart = Date.now();
|
||||
await this.store.addStep({
|
||||
runId: run.id,
|
||||
@@ -196,24 +234,32 @@ export class ReviewOrchestrator {
|
||||
startedAt: new Date(agentStart).toISOString(),
|
||||
});
|
||||
|
||||
// 使用Reflection模式运行specialists
|
||||
const enableReflection = config.review.enableReflection ?? false;
|
||||
const maxReflectionRounds = config.review.maxReflectionRounds ?? 2;
|
||||
|
||||
const agentResults = await Promise.all([
|
||||
enableReflection
|
||||
? this.correctnessAgent.reviewWithReflection(run, context, maxReflectionRounds)
|
||||
: this.correctnessAgent.review(run, context),
|
||||
enableReflection
|
||||
? this.securityAgent.reviewWithReflection(run, context, maxReflectionRounds)
|
||||
: this.securityAgent.review(run, context),
|
||||
enableReflection
|
||||
? this.reliabilityAgent.reviewWithReflection(run, context, maxReflectionRounds)
|
||||
: this.reliabilityAgent.review(run, context),
|
||||
enableReflection
|
||||
? this.maintainabilityAgent.reviewWithReflection(run, context, maxReflectionRounds)
|
||||
: this.maintainabilityAgent.review(run, context),
|
||||
]);
|
||||
// Select agents based on triage result
|
||||
const agentsToRun = triage
|
||||
? triage.relevantDomains.map((domain) => this.agentMap[domain]).filter(Boolean)
|
||||
: [this.correctnessAgent, this.securityAgent, this.reliabilityAgent, this.maintainabilityAgent];
|
||||
|
||||
// For trivial changes, skip reflection even if globally enabled
|
||||
const useReflection = triage?.complexity === 'trivial' ? false : enableReflection;
|
||||
|
||||
logger.info('Specialist 派发决策', {
|
||||
runId: run.id,
|
||||
triageComplexity: triage?.complexity ?? 'disabled',
|
||||
agentCount: agentsToRun.length,
|
||||
domains: triage?.relevantDomains ?? ['correctness', 'security', 'reliability', 'maintainability'],
|
||||
reflection: useReflection,
|
||||
});
|
||||
|
||||
const agentResults = await Promise.all(
|
||||
agentsToRun.map((agent) =>
|
||||
useReflection
|
||||
? agent.reviewWithReflection(run, context, maxReflectionRounds)
|
||||
: agent.review(run, context)
|
||||
)
|
||||
);
|
||||
|
||||
await this.store.addStep({
|
||||
runId: run.id,
|
||||
@@ -226,11 +272,11 @@ export class ReviewOrchestrator {
|
||||
|
||||
let allFindings = agentResults.flatMap((result) => result.findings);
|
||||
|
||||
// 对高严重性findings启动Debate
|
||||
// 对高严重性findings启动Debate(trivial 变更跳过 debate)
|
||||
const enableDebate = config.review.enableDebate ?? false;
|
||||
const debateThreshold = config.review.debateThreshold ?? 'high';
|
||||
|
||||
if (enableDebate && allFindings.length > 0) {
|
||||
if (enableDebate && allFindings.length > 0 && triage?.complexity !== 'trivial') {
|
||||
const debateStart = Date.now();
|
||||
await this.store.addStep({
|
||||
runId: run.id,
|
||||
|
||||
Reference in New Issue
Block a user