171 lines
4.9 KiB
TypeScript
171 lines
4.9 KiB
TypeScript
import type { AgentResponse, PartDefinition, PieceRule, RuleMatchMethod, Language } from '../models/types.js';
|
|
import { runAgent, type RunAgentOptions } from '../../agents/runner.js';
|
|
import { detectJudgeIndex, buildJudgePrompt } from '../../agents/judge-utils.js';
|
|
import { parseParts } from './engine/task-decomposer.js';
|
|
import { loadJudgmentSchema, loadEvaluationSchema, loadDecompositionSchema } from './schema-loader.js';
|
|
import { detectRuleIndex } from '../../shared/utils/ruleIndex.js';
|
|
import { ensureUniquePartIds, parsePartDefinitionEntry } from './part-definition-validator.js';
|
|
|
|
export interface JudgeStatusOptions {
|
|
cwd: string;
|
|
movementName: string;
|
|
language?: Language;
|
|
}
|
|
|
|
export interface JudgeStatusResult {
|
|
ruleIndex: number;
|
|
method: RuleMatchMethod;
|
|
}
|
|
|
|
export interface EvaluateConditionOptions {
|
|
cwd: string;
|
|
}
|
|
|
|
export interface DecomposeTaskOptions {
|
|
cwd: string;
|
|
persona?: string;
|
|
language?: Language;
|
|
model?: string;
|
|
provider?: 'claude' | 'codex' | 'opencode' | 'mock';
|
|
}
|
|
|
|
function toPartDefinitions(raw: unknown, maxParts: number): PartDefinition[] {
|
|
if (!Array.isArray(raw)) {
|
|
throw new Error('Structured output "parts" must be an array');
|
|
}
|
|
if (raw.length === 0) {
|
|
throw new Error('Structured output "parts" must not be empty');
|
|
}
|
|
if (raw.length > maxParts) {
|
|
throw new Error(`Structured output produced too many parts: ${raw.length} > ${maxParts}`);
|
|
}
|
|
|
|
const parts: PartDefinition[] = raw.map((entry, index) => parsePartDefinitionEntry(entry, index));
|
|
ensureUniquePartIds(parts);
|
|
|
|
return parts;
|
|
}
|
|
|
|
export async function executeAgent(
|
|
persona: string | undefined,
|
|
instruction: string,
|
|
options: RunAgentOptions,
|
|
): Promise<AgentResponse> {
|
|
return runAgent(persona, instruction, options);
|
|
}
|
|
export const generateReport = executeAgent;
|
|
export const executePart = executeAgent;
|
|
|
|
export async function evaluateCondition(
|
|
agentOutput: string,
|
|
conditions: Array<{ index: number; text: string }>,
|
|
options: EvaluateConditionOptions,
|
|
): Promise<number> {
|
|
const prompt = buildJudgePrompt(agentOutput, conditions);
|
|
const response = await runAgent(undefined, prompt, {
|
|
cwd: options.cwd,
|
|
maxTurns: 1,
|
|
permissionMode: 'readonly',
|
|
outputSchema: loadEvaluationSchema(),
|
|
});
|
|
|
|
if (response.status !== 'done') {
|
|
return -1;
|
|
}
|
|
|
|
const matchedIndex = response.structuredOutput?.matched_index;
|
|
if (typeof matchedIndex === 'number' && Number.isInteger(matchedIndex)) {
|
|
const zeroBased = matchedIndex - 1;
|
|
if (zeroBased >= 0 && zeroBased < conditions.length) {
|
|
return zeroBased;
|
|
}
|
|
}
|
|
|
|
return detectJudgeIndex(response.content);
|
|
}
|
|
|
|
export async function judgeStatus(
|
|
instruction: string,
|
|
rules: PieceRule[],
|
|
options: JudgeStatusOptions,
|
|
): Promise<JudgeStatusResult> {
|
|
if (rules.length === 0) {
|
|
throw new Error('judgeStatus requires at least one rule');
|
|
}
|
|
|
|
if (rules.length === 1) {
|
|
return {
|
|
ruleIndex: 0,
|
|
method: 'auto_select',
|
|
};
|
|
}
|
|
|
|
const response = await runAgent('conductor', instruction, {
|
|
cwd: options.cwd,
|
|
maxTurns: 3,
|
|
permissionMode: 'readonly',
|
|
language: options.language,
|
|
outputSchema: loadJudgmentSchema(),
|
|
});
|
|
|
|
if (response.status === 'done') {
|
|
const stepNumber = response.structuredOutput?.step;
|
|
if (typeof stepNumber === 'number' && Number.isInteger(stepNumber)) {
|
|
const ruleIndex = stepNumber - 1;
|
|
if (ruleIndex >= 0 && ruleIndex < rules.length) {
|
|
return {
|
|
ruleIndex,
|
|
method: 'structured_output',
|
|
};
|
|
}
|
|
}
|
|
|
|
const tagRuleIndex = detectRuleIndex(response.content, options.movementName);
|
|
if (tagRuleIndex >= 0 && tagRuleIndex < rules.length) {
|
|
return {
|
|
ruleIndex: tagRuleIndex,
|
|
method: 'phase3_tag',
|
|
};
|
|
}
|
|
}
|
|
|
|
const conditions = rules.map((rule, index) => ({ index, text: rule.condition }));
|
|
const fallbackIndex = await evaluateCondition(instruction, conditions, { cwd: options.cwd });
|
|
if (fallbackIndex >= 0 && fallbackIndex < rules.length) {
|
|
return {
|
|
ruleIndex: fallbackIndex,
|
|
method: 'ai_judge',
|
|
};
|
|
}
|
|
|
|
throw new Error(`Status not found for movement "${options.movementName}"`);
|
|
}
|
|
|
|
export async function decomposeTask(
|
|
instruction: string,
|
|
maxParts: number,
|
|
options: DecomposeTaskOptions,
|
|
): Promise<PartDefinition[]> {
|
|
const response = await runAgent(options.persona, instruction, {
|
|
cwd: options.cwd,
|
|
language: options.language,
|
|
model: options.model,
|
|
provider: options.provider,
|
|
permissionMode: 'readonly',
|
|
maxTurns: 3,
|
|
outputSchema: loadDecompositionSchema(maxParts),
|
|
});
|
|
|
|
if (response.status !== 'done') {
|
|
const detail = response.error ?? response.content;
|
|
throw new Error(`Team leader failed: ${detail}`);
|
|
}
|
|
|
|
const parts = response.structuredOutput?.parts;
|
|
if (parts != null) {
|
|
return toPartDefinitions(parts, maxParts);
|
|
}
|
|
|
|
return parseParts(response.content, maxParts);
|
|
}
|