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]');
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const result = await judgeStatus('instruction', [{ condition: 'always', next: 'done' }], {
|
||||
cwd: '/repo',
|
||||
@ -96,6 +112,13 @@ describe('agent-usecases', () => {
|
||||
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 () => {
|
||||
vi.mocked(runAgent).mockResolvedValue(doneResponse('x', { step: 2 }));
|
||||
|
||||
@ -141,6 +164,21 @@ describe('agent-usecases', () => {
|
||||
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 () => {
|
||||
vi.mocked(runAgent).mockResolvedValue(doneResponse('x', {
|
||||
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 { parseParts } from './engine/task-decomposer.js';
|
||||
import { loadJudgmentSchema, loadEvaluationSchema, loadDecompositionSchema } from './schema-loader.js';
|
||||
|
||||
export type UsecaseOptions = RunAgentOptions;
|
||||
import { detectRuleIndex } from '../../shared/utils/ruleIndex.js';
|
||||
import { ensureUniquePartIds, parsePartDefinitionEntry } from './part-definition-validator.js';
|
||||
|
||||
export interface JudgeStatusOptions {
|
||||
cwd: string;
|
||||
@ -29,18 +29,6 @@ export interface DecomposeTaskOptions {
|
||||
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[] {
|
||||
if (!Array.isArray(raw)) {
|
||||
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}`);
|
||||
}
|
||||
|
||||
const parts: PartDefinition[] = raw.map((entry, index) => {
|
||||
if (typeof entry !== 'object' || entry == null || Array.isArray(entry)) {
|
||||
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);
|
||||
}
|
||||
const parts: PartDefinition[] = raw.map((entry, index) => parsePartDefinitionEntry(entry, index));
|
||||
ensureUniquePartIds(parts);
|
||||
|
||||
return parts;
|
||||
}
|
||||
@ -100,26 +49,12 @@ function toPartDefinitions(raw: unknown, maxParts: number): PartDefinition[] {
|
||||
export async function executeAgent(
|
||||
persona: string | undefined,
|
||||
instruction: string,
|
||||
options: UsecaseOptions,
|
||||
): 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,
|
||||
options: RunAgentOptions,
|
||||
): Promise<AgentResponse> {
|
||||
return runAgent(persona, instruction, options);
|
||||
}
|
||||
export const generateReport = executeAgent;
|
||||
export const executePart = executeAgent;
|
||||
|
||||
export async function evaluateCondition(
|
||||
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 type { PieceMovement, PieceState, Language } from '../../models/types.js';
|
||||
import type { RunAgentOptions } from '../../../agents/runner.js';
|
||||
|
||||
@ -5,7 +5,7 @@ import type {
|
||||
PartDefinition,
|
||||
PartResult,
|
||||
} 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 { buildSessionKey } from '../session-key.js';
|
||||
import { ParallelLogger } from './parallel-logger.js';
|
||||
@ -232,7 +232,7 @@ export class TeamLeaderRunner {
|
||||
: { ...baseOptions, abortSignal: signal };
|
||||
|
||||
try {
|
||||
const response = await executePart(partMovement.persona, part.instruction, options);
|
||||
const response = await executeAgent(partMovement.persona, part.instruction, options);
|
||||
updatePersonaSession(buildSessionKey(partMovement), response.sessionId);
|
||||
return {
|
||||
part,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
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;
|
||||
|
||||
@ -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[] {
|
||||
const parsed = parseJsonBlock(content);
|
||||
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}`);
|
||||
}
|
||||
|
||||
const parts = parsed.map((entry, index) => parsePartEntry(entry, index));
|
||||
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);
|
||||
}
|
||||
const parts = parsed.map((entry, index) => parsePartDefinitionEntry(entry, index));
|
||||
ensureUniquePartIds(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 { ReportInstructionBuilder } from './instruction/ReportInstructionBuilder.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 { buildSessionKey } from './session-key.js';
|
||||
export { runStatusJudgmentPhase } from './status-judgment-phase.js';
|
||||
@ -214,7 +214,7 @@ async function runSingleReportAttempt(
|
||||
|
||||
let response: AgentResponse;
|
||||
try {
|
||||
response = await generateReport(step.persona, instruction, options);
|
||||
response = await executeAgent(step.persona, instruction, options);
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
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 type { CodexCallOptions, CodexSandboxMode } from './infra/codex/index.js';
|
||||
|
||||
// Agent execution
|
||||
export * from './agents/index.js';
|
||||
|
||||
// Piece engine
|
||||
export {
|
||||
PieceEngine,
|
||||
@ -107,12 +104,6 @@ export {
|
||||
InstructionBuilder,
|
||||
isOutputContractItem,
|
||||
ReportInstructionBuilder,
|
||||
StatusJudgmentBuilder,
|
||||
buildEditRule,
|
||||
RuleEvaluator,
|
||||
evaluateAggregateConditions,
|
||||
AggregateEvaluator,
|
||||
needsStatusJudgmentPhase,
|
||||
executeAgent,
|
||||
generateReport,
|
||||
executePart,
|
||||
@ -129,13 +120,7 @@ export type {
|
||||
PieceEngineOptions,
|
||||
LoopCheckResult,
|
||||
ProviderType,
|
||||
RuleMatch,
|
||||
RuleEvaluatorContext,
|
||||
JudgeStatusResult,
|
||||
ReportInstructionContext,
|
||||
StatusJudgmentContext,
|
||||
InstructionContext,
|
||||
StatusRulesComponents,
|
||||
BlockedHandlerResult,
|
||||
} from './core/piece/index.js';
|
||||
|
||||
|
||||
@ -3,24 +3,9 @@
|
||||
*
|
||||
* Stateless helpers for rule detection and regex safety validation.
|
||||
*/
|
||||
import { detectRuleIndex } from '../../shared/utils/ruleIndex.js';
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
export { detectRuleIndex };
|
||||
|
||||
/** Validate regex pattern for ReDoS safety */
|
||||
export function isRegexSafe(pattern: string): boolean {
|
||||
|
||||
@ -156,7 +156,9 @@ export class CodexClient {
|
||||
if (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();
|
||||
diag.onConnected();
|
||||
|
||||
|
||||
@ -343,10 +343,11 @@ export class OpenCodeClient {
|
||||
};
|
||||
}
|
||||
|
||||
await client.session.promptAsync(
|
||||
promptPayload as never,
|
||||
{ signal: streamAbortController.signal },
|
||||
);
|
||||
// OpenCode SDK types do not yet expose outputFormat even though runtime accepts it.
|
||||
const promptPayloadForSdk = promptPayload as unknown as Parameters<typeof client.session.promptAsync>[0];
|
||||
await client.session.promptAsync(promptPayloadForSdk, {
|
||||
signal: streamAbortController.signal,
|
||||
});
|
||||
|
||||
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