takt: 257/add-agent-usecase-structured-o
This commit is contained in:
parent
bf4196d3b3
commit
bcf38a5530
@ -86,6 +86,22 @@ describe('agent-usecases', () => {
|
|||||||
expect(detectJudgeIndex).toHaveBeenCalledWith('[JUDGE:2]');
|
expect(detectJudgeIndex).toHaveBeenCalledWith('[JUDGE:2]');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('evaluateCondition は runAgent が done 以外なら -1 を返す', async () => {
|
||||||
|
vi.mocked(runAgent).mockResolvedValue({
|
||||||
|
persona: 'tester',
|
||||||
|
status: 'error',
|
||||||
|
content: 'failed',
|
||||||
|
timestamp: new Date('2026-02-12T00:00:00Z'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await evaluateCondition('agent output', [
|
||||||
|
{ index: 0, text: 'first' },
|
||||||
|
], { cwd: '/repo' });
|
||||||
|
|
||||||
|
expect(result).toBe(-1);
|
||||||
|
expect(detectJudgeIndex).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('judgeStatus は単一ルール時に auto_select を返す', async () => {
|
it('judgeStatus は単一ルール時に auto_select を返す', async () => {
|
||||||
const result = await judgeStatus('instruction', [{ condition: 'always', next: 'done' }], {
|
const result = await judgeStatus('instruction', [{ condition: 'always', next: 'done' }], {
|
||||||
cwd: '/repo',
|
cwd: '/repo',
|
||||||
@ -96,6 +112,13 @@ describe('agent-usecases', () => {
|
|||||||
expect(runAgent).not.toHaveBeenCalled();
|
expect(runAgent).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('judgeStatus はルールが空ならエラー', async () => {
|
||||||
|
await expect(judgeStatus('instruction', [], {
|
||||||
|
cwd: '/repo',
|
||||||
|
movementName: 'review',
|
||||||
|
})).rejects.toThrow('judgeStatus requires at least one rule');
|
||||||
|
});
|
||||||
|
|
||||||
it('judgeStatus は構造化出力 step を採用する', async () => {
|
it('judgeStatus は構造化出力 step を採用する', async () => {
|
||||||
vi.mocked(runAgent).mockResolvedValue(doneResponse('x', { step: 2 }));
|
vi.mocked(runAgent).mockResolvedValue(doneResponse('x', { step: 2 }));
|
||||||
|
|
||||||
@ -141,6 +164,21 @@ describe('agent-usecases', () => {
|
|||||||
expect(runAgent).toHaveBeenCalledTimes(2);
|
expect(runAgent).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('judgeStatus は全ての判定に失敗したらエラー', async () => {
|
||||||
|
vi.mocked(runAgent)
|
||||||
|
.mockResolvedValueOnce(doneResponse('no match'))
|
||||||
|
.mockResolvedValueOnce(doneResponse('still no match'));
|
||||||
|
vi.mocked(detectJudgeIndex).mockReturnValue(-1);
|
||||||
|
|
||||||
|
await expect(judgeStatus('instruction', [
|
||||||
|
{ condition: 'a', next: 'one' },
|
||||||
|
{ condition: 'b', next: 'two' },
|
||||||
|
], {
|
||||||
|
cwd: '/repo',
|
||||||
|
movementName: 'review',
|
||||||
|
})).rejects.toThrow('Status not found for movement "review"');
|
||||||
|
});
|
||||||
|
|
||||||
it('decomposeTask は構造化出力 parts を返す', async () => {
|
it('decomposeTask は構造化出力 parts を返す', async () => {
|
||||||
vi.mocked(runAgent).mockResolvedValue(doneResponse('x', {
|
vi.mocked(runAgent).mockResolvedValue(doneResponse('x', {
|
||||||
parts: [
|
parts: [
|
||||||
|
|||||||
@ -1,70 +0,0 @@
|
|||||||
/**
|
|
||||||
* Test for JudgmentDetector
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { JudgmentDetector } from '../core/piece/judgment/JudgmentDetector.js';
|
|
||||||
|
|
||||||
describe('JudgmentDetector', () => {
|
|
||||||
describe('detect', () => {
|
|
||||||
it('should detect tag in simple response', () => {
|
|
||||||
const result = JudgmentDetector.detect('[ARCH-REVIEW:1]');
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
expect(result.tag).toBe('[ARCH-REVIEW:1]');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect tag with surrounding text', () => {
|
|
||||||
const result = JudgmentDetector.detect('Based on the review, I choose [MOVEMENT:2] because...');
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
expect(result.tag).toBe('[MOVEMENT:2]');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect tag with hyphenated movement name', () => {
|
|
||||||
const result = JudgmentDetector.detect('[AI-ANTIPATTERN-REVIEW:1]');
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
expect(result.tag).toBe('[AI-ANTIPATTERN-REVIEW:1]');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect tag with underscored movement name', () => {
|
|
||||||
const result = JudgmentDetector.detect('[AI_REVIEW:1]');
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
expect(result.tag).toBe('[AI_REVIEW:1]');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect "判断できない" (Japanese)', () => {
|
|
||||||
const result = JudgmentDetector.detect('判断できない:情報が不足しています');
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
expect(result.reason).toBe('Conductor explicitly stated it cannot judge');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect "Cannot determine" (English)', () => {
|
|
||||||
const result = JudgmentDetector.detect('Cannot determine: Insufficient information');
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
expect(result.reason).toBe('Conductor explicitly stated it cannot judge');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect "unable to judge"', () => {
|
|
||||||
const result = JudgmentDetector.detect('I am unable to judge based on the provided information.');
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
expect(result.reason).toBe('Conductor explicitly stated it cannot judge');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fail when no tag and no explicit "cannot judge"', () => {
|
|
||||||
const result = JudgmentDetector.detect('This is a response without a tag or explicit statement.');
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
expect(result.reason).toBe('No tag found and no explicit "cannot judge" statement');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fail on empty response', () => {
|
|
||||||
const result = JudgmentDetector.detect('');
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
expect(result.reason).toBe('No tag found and no explicit "cannot judge" statement');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect first tag when multiple tags exist', () => {
|
|
||||||
const result = JudgmentDetector.detect('[MOVEMENT:1] or [MOVEMENT:2]');
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
expect(result.tag).toBe('[MOVEMENT:1]');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
40
src/__tests__/public-api-exports.test.ts
Normal file
40
src/__tests__/public-api-exports.test.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
describe('public API exports', () => {
|
||||||
|
it('should expose piece usecases and engine public APIs', async () => {
|
||||||
|
// Given: パッケージの公開API
|
||||||
|
const api = await import('../index.js');
|
||||||
|
|
||||||
|
// When: 主要なユースケース関数とエンジン公開APIを参照する
|
||||||
|
// Then: 必要な公開シンボルが利用できる
|
||||||
|
expect(typeof api.executeAgent).toBe('function');
|
||||||
|
expect(typeof api.generateReport).toBe('function');
|
||||||
|
expect(typeof api.executePart).toBe('function');
|
||||||
|
expect(typeof api.judgeStatus).toBe('function');
|
||||||
|
expect(typeof api.evaluateCondition).toBe('function');
|
||||||
|
expect(typeof api.decomposeTask).toBe('function');
|
||||||
|
|
||||||
|
expect(typeof api.PieceEngine).toBe('function');
|
||||||
|
expect(typeof api.createInitialState).toBe('function');
|
||||||
|
expect(typeof api.addUserInput).toBe('function');
|
||||||
|
expect(typeof api.getPreviousOutput).toBe('function');
|
||||||
|
expect(api.COMPLETE_MOVEMENT).toBeDefined();
|
||||||
|
expect(api.ABORT_MOVEMENT).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not expose internal engine implementation details', async () => {
|
||||||
|
// Given: パッケージの公開API
|
||||||
|
const api = await import('../index.js');
|
||||||
|
|
||||||
|
// When: 非公開にすべき内部シンボルの有無を確認する
|
||||||
|
// Then: 内部実装詳細は公開されていない
|
||||||
|
expect('AgentRunner' in api).toBe(false);
|
||||||
|
expect('RuleEvaluator' in api).toBe(false);
|
||||||
|
expect('AggregateEvaluator' in api).toBe(false);
|
||||||
|
expect('evaluateAggregateConditions' in api).toBe(false);
|
||||||
|
expect('needsStatusJudgmentPhase' in api).toBe(false);
|
||||||
|
expect('StatusJudgmentBuilder' in api).toBe(false);
|
||||||
|
expect('buildEditRule' in api).toBe(false);
|
||||||
|
expect('detectRuleIndex' in api).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -3,8 +3,8 @@ import { runAgent, type RunAgentOptions } from '../../agents/runner.js';
|
|||||||
import { detectJudgeIndex, buildJudgePrompt } from '../../agents/judge-utils.js';
|
import { detectJudgeIndex, buildJudgePrompt } from '../../agents/judge-utils.js';
|
||||||
import { parseParts } from './engine/task-decomposer.js';
|
import { parseParts } from './engine/task-decomposer.js';
|
||||||
import { loadJudgmentSchema, loadEvaluationSchema, loadDecompositionSchema } from './schema-loader.js';
|
import { loadJudgmentSchema, loadEvaluationSchema, loadDecompositionSchema } from './schema-loader.js';
|
||||||
|
import { detectRuleIndex } from '../../shared/utils/ruleIndex.js';
|
||||||
export type UsecaseOptions = RunAgentOptions;
|
import { ensureUniquePartIds, parsePartDefinitionEntry } from './part-definition-validator.js';
|
||||||
|
|
||||||
export interface JudgeStatusOptions {
|
export interface JudgeStatusOptions {
|
||||||
cwd: string;
|
cwd: string;
|
||||||
@ -29,18 +29,6 @@ export interface DecomposeTaskOptions {
|
|||||||
provider?: 'claude' | 'codex' | 'opencode' | 'mock';
|
provider?: 'claude' | 'codex' | 'opencode' | 'mock';
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectRuleIndex(content: string, movementName: string): number {
|
|
||||||
const tag = movementName.toUpperCase();
|
|
||||||
const regex = new RegExp(`\\[${tag}:(\\d+)\\]`, 'gi');
|
|
||||||
const matches = [...content.matchAll(regex)];
|
|
||||||
const match = matches.at(-1);
|
|
||||||
if (match?.[1]) {
|
|
||||||
const index = Number.parseInt(match[1], 10) - 1;
|
|
||||||
return index >= 0 ? index : -1;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toPartDefinitions(raw: unknown, maxParts: number): PartDefinition[] {
|
function toPartDefinitions(raw: unknown, maxParts: number): PartDefinition[] {
|
||||||
if (!Array.isArray(raw)) {
|
if (!Array.isArray(raw)) {
|
||||||
throw new Error('Structured output "parts" must be an array');
|
throw new Error('Structured output "parts" must be an array');
|
||||||
@ -52,47 +40,8 @@ function toPartDefinitions(raw: unknown, maxParts: number): PartDefinition[] {
|
|||||||
throw new Error(`Structured output produced too many parts: ${raw.length} > ${maxParts}`);
|
throw new Error(`Structured output produced too many parts: ${raw.length} > ${maxParts}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts: PartDefinition[] = raw.map((entry, index) => {
|
const parts: PartDefinition[] = raw.map((entry, index) => parsePartDefinitionEntry(entry, index));
|
||||||
if (typeof entry !== 'object' || entry == null || Array.isArray(entry)) {
|
ensureUniquePartIds(parts);
|
||||||
throw new Error(`Part[${index}] must be an object`);
|
|
||||||
}
|
|
||||||
const row = entry as Record<string, unknown>;
|
|
||||||
const id = row.id;
|
|
||||||
const title = row.title;
|
|
||||||
const instruction = row.instruction;
|
|
||||||
const timeoutMs = row.timeout_ms;
|
|
||||||
|
|
||||||
if (typeof id !== 'string' || id.trim().length === 0) {
|
|
||||||
throw new Error(`Part[${index}] "id" must be a non-empty string`);
|
|
||||||
}
|
|
||||||
if (typeof title !== 'string' || title.trim().length === 0) {
|
|
||||||
throw new Error(`Part[${index}] "title" must be a non-empty string`);
|
|
||||||
}
|
|
||||||
if (typeof instruction !== 'string' || instruction.trim().length === 0) {
|
|
||||||
throw new Error(`Part[${index}] "instruction" must be a non-empty string`);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
timeoutMs != null
|
|
||||||
&& (typeof timeoutMs !== 'number' || !Number.isInteger(timeoutMs) || timeoutMs <= 0)
|
|
||||||
) {
|
|
||||||
throw new Error(`Part[${index}] "timeout_ms" must be a positive integer`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
instruction,
|
|
||||||
timeoutMs: timeoutMs as number | undefined,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const seen = new Set<string>();
|
|
||||||
for (const part of parts) {
|
|
||||||
if (seen.has(part.id)) {
|
|
||||||
throw new Error(`Duplicate part id: ${part.id}`);
|
|
||||||
}
|
|
||||||
seen.add(part.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts;
|
return parts;
|
||||||
}
|
}
|
||||||
@ -100,26 +49,12 @@ function toPartDefinitions(raw: unknown, maxParts: number): PartDefinition[] {
|
|||||||
export async function executeAgent(
|
export async function executeAgent(
|
||||||
persona: string | undefined,
|
persona: string | undefined,
|
||||||
instruction: string,
|
instruction: string,
|
||||||
options: UsecaseOptions,
|
options: RunAgentOptions,
|
||||||
): Promise<AgentResponse> {
|
|
||||||
return runAgent(persona, instruction, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function generateReport(
|
|
||||||
persona: string | undefined,
|
|
||||||
instruction: string,
|
|
||||||
options: UsecaseOptions,
|
|
||||||
): Promise<AgentResponse> {
|
|
||||||
return runAgent(persona, instruction, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function executePart(
|
|
||||||
persona: string | undefined,
|
|
||||||
instruction: string,
|
|
||||||
options: UsecaseOptions,
|
|
||||||
): Promise<AgentResponse> {
|
): Promise<AgentResponse> {
|
||||||
return runAgent(persona, instruction, options);
|
return runAgent(persona, instruction, options);
|
||||||
}
|
}
|
||||||
|
export const generateReport = executeAgent;
|
||||||
|
export const executePart = executeAgent;
|
||||||
|
|
||||||
export async function evaluateCondition(
|
export async function evaluateCondition(
|
||||||
agentOutput: string,
|
agentOutput: string,
|
||||||
|
|||||||
@ -1,10 +1,3 @@
|
|||||||
/**
|
|
||||||
* Builds RunAgentOptions for different execution phases.
|
|
||||||
*
|
|
||||||
* Centralizes the option construction logic that was previously
|
|
||||||
* scattered across PieceEngine methods.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import type { PieceMovement, PieceState, Language } from '../../models/types.js';
|
import type { PieceMovement, PieceState, Language } from '../../models/types.js';
|
||||||
import type { RunAgentOptions } from '../../../agents/runner.js';
|
import type { RunAgentOptions } from '../../../agents/runner.js';
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import type {
|
|||||||
PartDefinition,
|
PartDefinition,
|
||||||
PartResult,
|
PartResult,
|
||||||
} from '../../models/types.js';
|
} from '../../models/types.js';
|
||||||
import { decomposeTask, executePart } from '../agent-usecases.js';
|
import { decomposeTask, executeAgent } from '../agent-usecases.js';
|
||||||
import { detectMatchedRule } from '../evaluation/index.js';
|
import { detectMatchedRule } from '../evaluation/index.js';
|
||||||
import { buildSessionKey } from '../session-key.js';
|
import { buildSessionKey } from '../session-key.js';
|
||||||
import { ParallelLogger } from './parallel-logger.js';
|
import { ParallelLogger } from './parallel-logger.js';
|
||||||
@ -232,7 +232,7 @@ export class TeamLeaderRunner {
|
|||||||
: { ...baseOptions, abortSignal: signal };
|
: { ...baseOptions, abortSignal: signal };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await executePart(partMovement.persona, part.instruction, options);
|
const response = await executeAgent(partMovement.persona, part.instruction, options);
|
||||||
updatePersonaSession(buildSessionKey(partMovement), response.sessionId);
|
updatePersonaSession(buildSessionKey(partMovement), response.sessionId);
|
||||||
return {
|
return {
|
||||||
part,
|
part,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import type { PartDefinition } from '../../models/part.js';
|
import type { PartDefinition } from '../../models/part.js';
|
||||||
|
import { ensureUniquePartIds, parsePartDefinitionEntry } from '../part-definition-validator.js';
|
||||||
|
|
||||||
const JSON_CODE_BLOCK_REGEX = /```json\s*([\s\S]*?)```/g;
|
const JSON_CODE_BLOCK_REGEX = /```json\s*([\s\S]*?)```/g;
|
||||||
|
|
||||||
@ -24,36 +25,6 @@ function parseJsonBlock(content: string): unknown {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertString(value: unknown, fieldName: string, index: number): string {
|
|
||||||
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
||||||
throw new Error(`Part[${index}] "${fieldName}" must be a non-empty string`);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parsePartEntry(entry: unknown, index: number): PartDefinition {
|
|
||||||
if (typeof entry !== 'object' || entry == null || Array.isArray(entry)) {
|
|
||||||
throw new Error(`Part[${index}] must be an object`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const raw = entry as Record<string, unknown>;
|
|
||||||
const id = assertString(raw.id, 'id', index);
|
|
||||||
const title = assertString(raw.title, 'title', index);
|
|
||||||
const instruction = assertString(raw.instruction, 'instruction', index);
|
|
||||||
|
|
||||||
const timeoutMs = raw.timeout_ms;
|
|
||||||
if (timeoutMs != null && (typeof timeoutMs !== 'number' || !Number.isInteger(timeoutMs) || timeoutMs <= 0)) {
|
|
||||||
throw new Error(`Part[${index}] "timeout_ms" must be a positive integer`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
instruction,
|
|
||||||
timeoutMs: timeoutMs as number | undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseParts(content: string, maxParts: number): PartDefinition[] {
|
export function parseParts(content: string, maxParts: number): PartDefinition[] {
|
||||||
const parsed = parseJsonBlock(content);
|
const parsed = parseJsonBlock(content);
|
||||||
if (!Array.isArray(parsed)) {
|
if (!Array.isArray(parsed)) {
|
||||||
@ -66,14 +37,8 @@ export function parseParts(content: string, maxParts: number): PartDefinition[]
|
|||||||
throw new Error(`Team leader produced too many parts: ${parsed.length} > ${maxParts}`);
|
throw new Error(`Team leader produced too many parts: ${parsed.length} > ${maxParts}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts = parsed.map((entry, index) => parsePartEntry(entry, index));
|
const parts = parsed.map((entry, index) => parsePartDefinitionEntry(entry, index));
|
||||||
const ids = new Set<string>();
|
ensureUniquePartIds(parts);
|
||||||
for (const part of parts) {
|
|
||||||
if (ids.has(part.id)) {
|
|
||||||
throw new Error(`Duplicate part id: ${part.id}`);
|
|
||||||
}
|
|
||||||
ids.add(part.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts;
|
return parts;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,45 +0,0 @@
|
|||||||
/**
|
|
||||||
* Detect judgment result from conductor's response.
|
|
||||||
*/
|
|
||||||
export interface JudgmentResult {
|
|
||||||
success: boolean;
|
|
||||||
tag?: string; // e.g., "[ARCH-REVIEW:1]"
|
|
||||||
reason?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class JudgmentDetector {
|
|
||||||
private static readonly TAG_PATTERN = /\[([A-Z_-]+):(\d+)\]/;
|
|
||||||
private static readonly CANNOT_JUDGE_PATTERNS = [
|
|
||||||
/判断できない/i,
|
|
||||||
/cannot\s+determine/i,
|
|
||||||
/unable\s+to\s+judge/i,
|
|
||||||
/insufficient\s+information/i,
|
|
||||||
];
|
|
||||||
|
|
||||||
static detect(response: string): JudgmentResult {
|
|
||||||
// 1. タグ検出
|
|
||||||
const tagMatch = response.match(this.TAG_PATTERN);
|
|
||||||
if (tagMatch) {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
tag: tagMatch[0], // e.g., "[ARCH-REVIEW:1]"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 「判断できない」検出
|
|
||||||
for (const pattern of this.CANNOT_JUDGE_PATTERNS) {
|
|
||||||
if (pattern.test(response)) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
reason: 'Conductor explicitly stated it cannot judge',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. タグも「判断できない」もない → 失敗
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
reason: 'No tag found and no explicit "cannot judge" statement',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
/**
|
|
||||||
* Judgment module exports
|
|
||||||
*/
|
|
||||||
|
|
||||||
export {
|
|
||||||
JudgmentDetector,
|
|
||||||
type JudgmentResult,
|
|
||||||
} from './JudgmentDetector.js';
|
|
||||||
41
src/core/piece/part-definition-validator.ts
Normal file
41
src/core/piece/part-definition-validator.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import type { PartDefinition } from '../models/part.js';
|
||||||
|
|
||||||
|
function assertNonEmptyString(value: unknown, fieldName: string, index: number): string {
|
||||||
|
if (typeof value !== 'string' || value.trim().length === 0) {
|
||||||
|
throw new Error(`Part[${index}] "${fieldName}" must be a non-empty string`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parsePartDefinitionEntry(entry: unknown, index: number): PartDefinition {
|
||||||
|
if (typeof entry !== 'object' || entry == null || Array.isArray(entry)) {
|
||||||
|
throw new Error(`Part[${index}] must be an object`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = entry as Record<string, unknown>;
|
||||||
|
const id = assertNonEmptyString(raw.id, 'id', index);
|
||||||
|
const title = assertNonEmptyString(raw.title, 'title', index);
|
||||||
|
const instruction = assertNonEmptyString(raw.instruction, 'instruction', index);
|
||||||
|
|
||||||
|
const timeoutMs = raw.timeout_ms;
|
||||||
|
if (timeoutMs != null && (typeof timeoutMs !== 'number' || !Number.isInteger(timeoutMs) || timeoutMs <= 0)) {
|
||||||
|
throw new Error(`Part[${index}] "timeout_ms" must be a positive integer`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
instruction,
|
||||||
|
timeoutMs: timeoutMs as number | undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureUniquePartIds(parts: PartDefinition[]): void {
|
||||||
|
const ids = new Set<string>();
|
||||||
|
for (const part of parts) {
|
||||||
|
if (ids.has(part.id)) {
|
||||||
|
throw new Error(`Duplicate part id: ${part.id}`);
|
||||||
|
}
|
||||||
|
ids.add(part.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,7 +12,7 @@ import type { PhaseName } from './types.js';
|
|||||||
import type { RunAgentOptions } from '../../agents/runner.js';
|
import type { RunAgentOptions } from '../../agents/runner.js';
|
||||||
import { ReportInstructionBuilder } from './instruction/ReportInstructionBuilder.js';
|
import { ReportInstructionBuilder } from './instruction/ReportInstructionBuilder.js';
|
||||||
import { hasTagBasedRules, getReportFiles } from './evaluation/rule-utils.js';
|
import { hasTagBasedRules, getReportFiles } from './evaluation/rule-utils.js';
|
||||||
import { generateReport } from './agent-usecases.js';
|
import { executeAgent } from './agent-usecases.js';
|
||||||
import { createLogger } from '../../shared/utils/index.js';
|
import { createLogger } from '../../shared/utils/index.js';
|
||||||
import { buildSessionKey } from './session-key.js';
|
import { buildSessionKey } from './session-key.js';
|
||||||
export { runStatusJudgmentPhase } from './status-judgment-phase.js';
|
export { runStatusJudgmentPhase } from './status-judgment-phase.js';
|
||||||
@ -214,7 +214,7 @@ async function runSingleReportAttempt(
|
|||||||
|
|
||||||
let response: AgentResponse;
|
let response: AgentResponse;
|
||||||
try {
|
try {
|
||||||
response = await generateReport(step.persona, instruction, options);
|
response = await executeAgent(step.persona, instruction, options);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
ctx.onPhaseComplete?.(step, 2, 'report', '', 'error', errorMsg);
|
ctx.onPhaseComplete?.(step, 2, 'report', '', 'error', errorMsg);
|
||||||
|
|||||||
15
src/index.ts
15
src/index.ts
@ -87,9 +87,6 @@ export type {
|
|||||||
export { CodexClient, mapToCodexSandboxMode } from './infra/codex/index.js';
|
export { CodexClient, mapToCodexSandboxMode } from './infra/codex/index.js';
|
||||||
export type { CodexCallOptions, CodexSandboxMode } from './infra/codex/index.js';
|
export type { CodexCallOptions, CodexSandboxMode } from './infra/codex/index.js';
|
||||||
|
|
||||||
// Agent execution
|
|
||||||
export * from './agents/index.js';
|
|
||||||
|
|
||||||
// Piece engine
|
// Piece engine
|
||||||
export {
|
export {
|
||||||
PieceEngine,
|
PieceEngine,
|
||||||
@ -107,12 +104,6 @@ export {
|
|||||||
InstructionBuilder,
|
InstructionBuilder,
|
||||||
isOutputContractItem,
|
isOutputContractItem,
|
||||||
ReportInstructionBuilder,
|
ReportInstructionBuilder,
|
||||||
StatusJudgmentBuilder,
|
|
||||||
buildEditRule,
|
|
||||||
RuleEvaluator,
|
|
||||||
evaluateAggregateConditions,
|
|
||||||
AggregateEvaluator,
|
|
||||||
needsStatusJudgmentPhase,
|
|
||||||
executeAgent,
|
executeAgent,
|
||||||
generateReport,
|
generateReport,
|
||||||
executePart,
|
executePart,
|
||||||
@ -129,13 +120,7 @@ export type {
|
|||||||
PieceEngineOptions,
|
PieceEngineOptions,
|
||||||
LoopCheckResult,
|
LoopCheckResult,
|
||||||
ProviderType,
|
ProviderType,
|
||||||
RuleMatch,
|
|
||||||
RuleEvaluatorContext,
|
|
||||||
JudgeStatusResult,
|
JudgeStatusResult,
|
||||||
ReportInstructionContext,
|
|
||||||
StatusJudgmentContext,
|
|
||||||
InstructionContext,
|
|
||||||
StatusRulesComponents,
|
|
||||||
BlockedHandlerResult,
|
BlockedHandlerResult,
|
||||||
} from './core/piece/index.js';
|
} from './core/piece/index.js';
|
||||||
|
|
||||||
|
|||||||
@ -3,24 +3,9 @@
|
|||||||
*
|
*
|
||||||
* Stateless helpers for rule detection and regex safety validation.
|
* Stateless helpers for rule detection and regex safety validation.
|
||||||
*/
|
*/
|
||||||
|
import { detectRuleIndex } from '../../shared/utils/ruleIndex.js';
|
||||||
|
|
||||||
/**
|
export { detectRuleIndex };
|
||||||
* Detect rule index from numbered tag pattern [STEP_NAME:N].
|
|
||||||
* Returns 0-based rule index, or -1 if no match.
|
|
||||||
*
|
|
||||||
* Example: detectRuleIndex("... [PLAN:2] ...", "plan") → 1
|
|
||||||
*/
|
|
||||||
export function detectRuleIndex(content: string, movementName: string): number {
|
|
||||||
const tag = movementName.toUpperCase();
|
|
||||||
const regex = new RegExp(`\\[${tag}:(\\d+)\\]`, 'gi');
|
|
||||||
const matches = [...content.matchAll(regex)];
|
|
||||||
const match = matches.at(-1);
|
|
||||||
if (match?.[1]) {
|
|
||||||
const index = Number.parseInt(match[1], 10) - 1;
|
|
||||||
return index >= 0 ? index : -1;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Validate regex pattern for ReDoS safety */
|
/** Validate regex pattern for ReDoS safety */
|
||||||
export function isRegexSafe(pattern: string): boolean {
|
export function isRegexSafe(pattern: string): boolean {
|
||||||
|
|||||||
@ -156,7 +156,9 @@ export class CodexClient {
|
|||||||
if (options.outputSchema) {
|
if (options.outputSchema) {
|
||||||
runOptions.outputSchema = options.outputSchema;
|
runOptions.outputSchema = options.outputSchema;
|
||||||
}
|
}
|
||||||
const { events } = await thread.runStreamed(fullPrompt, runOptions as never);
|
// Codex SDK types do not yet expose outputSchema even though runtime accepts it.
|
||||||
|
const runStreamedOptions = runOptions as unknown as Parameters<typeof thread.runStreamed>[1];
|
||||||
|
const { events } = await thread.runStreamed(fullPrompt, runStreamedOptions);
|
||||||
resetIdleTimeout();
|
resetIdleTimeout();
|
||||||
diag.onConnected();
|
diag.onConnected();
|
||||||
|
|
||||||
|
|||||||
@ -343,10 +343,11 @@ export class OpenCodeClient {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.session.promptAsync(
|
// OpenCode SDK types do not yet expose outputFormat even though runtime accepts it.
|
||||||
promptPayload as never,
|
const promptPayloadForSdk = promptPayload as unknown as Parameters<typeof client.session.promptAsync>[0];
|
||||||
{ signal: streamAbortController.signal },
|
await client.session.promptAsync(promptPayloadForSdk, {
|
||||||
);
|
signal: streamAbortController.signal,
|
||||||
|
});
|
||||||
|
|
||||||
emitInit(options.onStream, options.model, sessionId);
|
emitInit(options.onStream, options.model, sessionId);
|
||||||
|
|
||||||
|
|||||||
15
src/shared/utils/ruleIndex.ts
Normal file
15
src/shared/utils/ruleIndex.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Detect rule index from numbered tag pattern [STEP_NAME:N].
|
||||||
|
* Returns 0-based rule index, or -1 if no match.
|
||||||
|
*/
|
||||||
|
export function detectRuleIndex(content: string, movementName: string): number {
|
||||||
|
const tag = movementName.toUpperCase();
|
||||||
|
const regex = new RegExp(`\\[${tag}:(\\d+)\\]`, 'gi');
|
||||||
|
const matches = [...content.matchAll(regex)];
|
||||||
|
const match = matches.at(-1);
|
||||||
|
if (match?.[1]) {
|
||||||
|
const index = Number.parseInt(match[1], 10) - 1;
|
||||||
|
return index >= 0 ? index : -1;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user