hotfix: 過去のprevious_responseがバインドされてしまう問題

This commit is contained in:
nrslib 2026-02-04 09:24:50 +09:00
parent 1b0a84601d
commit 9159f071f4
11 changed files with 141 additions and 5 deletions

View File

@ -8,7 +8,9 @@ You are responsible for instruction creation in TAKT's interactive mode. Convert
## Requirements
- Output only the final task instruction (no preamble).
- Be specific about scope and targets (files/modules) if mentioned.
- Preserve user-provided constraints and "do not" instructions.
- Preserve user-provided constraints and "do not" instructions **only if explicitly stated by the user**.
- If the source of a constraint is unclear, do not include it; add it to Open Questions if needed.
- Do not include constraints proposed or inferred by the assistant.
- Do NOT include assistant/system operational constraints (tool limits, execution prohibitions).
- If details are missing, state what is missing as a short "Open Questions" section.
- Clearly specify the concrete work that the workflow will execute.

View File

@ -9,6 +9,8 @@
- 出力はタスク指示書のみ(前置き不要)
- スコープや対象(ファイル/モジュール)が出ている場合は明確に書く
- ユーザー由来の制約や「やらないこと」は保持する
- 制約の出所が不明な場合は保持せず、必要なら Open Questions に回す
- アシスタントが提案・推測した制約は指示書に含めない
- アシスタントの運用上の制約(実行禁止/ツール制限など)は指示に含めない
- 情報不足があれば「Open Questions」セクションを短く付ける
- ワークフローが実行する具体的な作業内容を明記する

View File

@ -149,6 +149,128 @@ describe('WorkflowEngine Integration: Happy Path', () => {
// plan, implement, ai_review, reviewers(1st), fix, reviewers(2nd), supervise = 7
expect(state.iteration).toBe(7);
});
it('should inject latest reviewers output as Previous Response for repeated fix steps', async () => {
const config = buildDefaultWorkflowConfig();
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
mockRunAgentSequence([
makeResponse({ agent: 'plan', content: 'Plan done' }),
makeResponse({ agent: 'implement', content: 'Impl done' }),
makeResponse({ agent: 'ai_review', content: 'No issues' }),
// Round 1 reviewers
makeResponse({ agent: 'arch-review', content: 'Arch R1 OK' }),
makeResponse({ agent: 'security-review', content: 'Sec R1 needs fix' }),
// fix round 1
makeResponse({ agent: 'fix', content: 'Fix R1' }),
// Round 2 reviewers
makeResponse({ agent: 'arch-review', content: 'Arch R2 OK' }),
makeResponse({ agent: 'security-review', content: 'Sec R2 still failing' }),
// fix round 2
makeResponse({ agent: 'fix', content: 'Fix R2' }),
// Round 3 reviewers (approved)
makeResponse({ agent: 'arch-review', content: 'Arch R3 OK' }),
makeResponse({ agent: 'security-review', content: 'Sec R3 OK' }),
// supervise
makeResponse({ agent: 'supervise', content: 'All passed' }),
]);
mockDetectMatchedRuleSequence([
{ index: 0, method: 'phase1_tag' }, // plan → implement
{ index: 0, method: 'phase1_tag' }, // implement → ai_review
{ index: 0, method: 'phase1_tag' }, // ai_review → reviewers
{ index: 0, method: 'phase1_tag' }, // arch-review → approved
{ index: 1, method: 'phase1_tag' }, // security-review → needs_fix
{ index: 1, method: 'aggregate' }, // reviewers: any(needs_fix) → fix
{ index: 0, method: 'phase1_tag' }, // fix → reviewers
{ index: 0, method: 'phase1_tag' }, // arch-review → approved
{ index: 1, method: 'phase1_tag' }, // security-review → needs_fix
{ index: 1, method: 'aggregate' }, // reviewers: any(needs_fix) → fix
{ index: 0, method: 'phase1_tag' }, // fix → reviewers
{ index: 0, method: 'phase1_tag' }, // arch-review → approved
{ index: 0, method: 'phase1_tag' }, // security-review → approved
{ index: 0, method: 'aggregate' }, // reviewers: all(approved) → supervise
{ index: 0, method: 'phase1_tag' }, // supervise → COMPLETE
]);
const fixInstructions: string[] = [];
engine.on('movement:start', (step, _iteration, instruction) => {
if (step.name === 'fix') {
fixInstructions.push(instruction);
}
});
await engine.run();
expect(fixInstructions).toHaveLength(2);
const fix1 = fixInstructions[0]!;
expect(fix1).toContain('## Previous Response');
expect(fix1).toContain('Arch R1 OK');
expect(fix1).toContain('Sec R1 needs fix');
expect(fix1).not.toContain('Arch R2 OK');
expect(fix1).not.toContain('Sec R2 still failing');
const fix2 = fixInstructions[1]!;
expect(fix2).toContain('## Previous Response');
expect(fix2).toContain('Arch R2 OK');
expect(fix2).toContain('Sec R2 still failing');
expect(fix2).not.toContain('Arch R1 OK');
expect(fix2).not.toContain('Sec R1 needs fix');
});
it('should use the latest movement output across different steps for Previous Response', async () => {
const config = buildDefaultWorkflowConfig();
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
mockRunAgentSequence([
makeResponse({ agent: 'plan', content: 'Plan done' }),
makeResponse({ agent: 'implement', content: 'Impl done' }),
makeResponse({ agent: 'ai_review', content: 'AI issues found' }),
// ai_fix (should see ai_review output)
makeResponse({ agent: 'ai_fix', content: 'AI issues fixed' }),
// reviewers (approved)
makeResponse({ agent: 'arch-review', content: 'Arch OK' }),
makeResponse({ agent: 'security-review', content: 'Sec OK' }),
// supervise (should see reviewers aggregate output)
makeResponse({ agent: 'supervise', content: 'All passed' }),
]);
mockDetectMatchedRuleSequence([
{ index: 0, method: 'phase1_tag' }, // plan → implement
{ index: 0, method: 'phase1_tag' }, // implement → ai_review
{ index: 1, method: 'phase1_tag' }, // ai_review → ai_fix
{ index: 0, method: 'phase1_tag' }, // ai_fix → reviewers
{ index: 0, method: 'phase1_tag' }, // arch-review → approved
{ index: 0, method: 'phase1_tag' }, // security-review → approved
{ index: 0, method: 'aggregate' }, // reviewers → supervise
{ index: 0, method: 'phase1_tag' }, // supervise → COMPLETE
]);
const aiFixInstructions: string[] = [];
const superviseInstructions: string[] = [];
engine.on('movement:start', (step, _iteration, instruction) => {
if (step.name === 'ai_fix') {
aiFixInstructions.push(instruction);
} else if (step.name === 'supervise') {
superviseInstructions.push(instruction);
}
});
await engine.run();
expect(aiFixInstructions).toHaveLength(1);
const aiFix = aiFixInstructions[0]!;
expect(aiFix).toContain('## Previous Response');
expect(aiFix).toContain('AI issues found');
expect(aiFix).not.toContain('AI issues fixed');
expect(superviseInstructions).toHaveLength(1);
const supervise = superviseInstructions[0]!;
expect(supervise).toContain('## Previous Response');
expect(supervise).toContain('Arch OK');
expect(supervise).toContain('Sec OK');
});
});
// =====================================================

View File

@ -66,6 +66,7 @@ function makeState(movementOutputs?: Map<string, AgentResponse>): WorkflowState
iteration: 1,
status: 'running',
movementOutputs: movementOutputs ?? new Map(),
lastOutput: undefined,
movementIterations: new Map(),
agentSessions: new Map(),
userInputs: [],

View File

@ -114,6 +114,8 @@ export interface WorkflowState {
currentMovement: string;
iteration: number;
movementOutputs: Map<string, AgentResponse>;
/** Most recent movement output (used for Previous Response injection) */
lastOutput?: AgentResponse;
userInputs: string[];
agentSessions: Map<string, string>;
/** Per-movement iteration counters (how many times each movement has been executed) */

View File

@ -134,6 +134,7 @@ export class MovementExecutor {
}
state.movementOutputs.set(step.name, response);
state.lastOutput = response;
this.emitMovementReports(step);
return { response, instruction };
}
@ -174,4 +175,3 @@ export class MovementExecutor {
}
}

View File

@ -160,6 +160,7 @@ export class ParallelRunner {
};
state.movementOutputs.set(step.name, aggregatedResponse);
state.lastOutput = aggregatedResponse;
this.deps.movementExecutor.emitMovementReports(step);
return { response: aggregatedResponse, instruction: aggregatedInstruction };
}

View File

@ -39,6 +39,7 @@ export class StateManager {
currentMovement: config.initialMovement,
iteration: 0,
movementOutputs: new Map(),
lastOutput: undefined,
userInputs,
agentSessions,
movementIterations: new Map(),
@ -111,6 +112,7 @@ export function addUserInput(state: WorkflowState, input: string): void {
* Get the most recent movement output.
*/
export function getPreviousOutput(state: WorkflowState): AgentResponse | undefined {
if (state.lastOutput) return state.lastOutput;
const outputs = Array.from(state.movementOutputs.values());
return outputs[outputs.length - 1];
}

View File

@ -241,7 +241,7 @@ export async function executeWorkflow(
phase,
phaseName,
status: phaseStatus,
...(content ? { content } : {}),
content: content ?? '',
timestamp: new Date().toISOString(),
...(phaseError ? { error: phaseError } : {}),
};

View File

@ -9,7 +9,9 @@ You are a task summarizer. Convert the conversation into a concrete task instruc
Requirements:
- Output only the final task instruction (no preamble).
- Be specific about scope and targets (files/modules) if mentioned.
- Preserve constraints and "do not" instructions.
- Preserve constraints and "do not" instructions **only if they were explicitly stated by the user**.
- If the source of a constraint is unclear, do not include it; add it to Open Questions if needed.
- Do not include constraints proposed or inferred by the assistant.
- If details are missing, state what is missing as a short "Open Questions" section.
{{#if workflowInfo}}

View File

@ -16,7 +16,9 @@
- 対象ファイル/モジュールごとに作業内容を明記する
- 優先度(高/中/低)を付けて整理する
- 再現手順や確認方法があれば含める
- 制約や「やらないこと」を保持する
- 制約や「やらないこと」は**ユーザーが明示したもののみ**保持する
- 制約の出所が不明な場合は保持せず、必要なら Open Questions に回す
- アシスタントが提案・推測した制約は指示書に含めない
- 情報不足があれば「Open Questions」セクションを短く付ける
{{#if workflowInfo}}