실제 provider 요청과 작업 공간 handoff가 같은 PLAN/REVIEW 계약을 사용해야 하며, 단계별 prepare와 pair-write 의미가 테스트와 런타임에서 일치해야 한다.
97 lines
3.3 KiB
Go
97 lines
3.3 KiB
Go
package openai
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
func (s *Server) advanceHotPathReview(
|
|
ctx context.Context,
|
|
requestID string,
|
|
phase hotPathLightPhase,
|
|
output normalizedStageOutput,
|
|
visible normalizedStageOutput,
|
|
outer *hotPathOuterTurn,
|
|
protocol string,
|
|
) (normalizedStageOutput, bool, error) {
|
|
evidence, err := s.lightFlows.reviewEvidence(requestID, s.edgeIDValue())
|
|
if err != nil {
|
|
return normalizedStageOutput{}, false, err
|
|
}
|
|
kind, cleanup, err := classifyHotPathReviewOutput(requestID, phase, output, evidence)
|
|
if err != nil {
|
|
return normalizedStageOutput{}, false, err
|
|
}
|
|
if cleanup {
|
|
terminalOutput := output.StageResponseOverlay(visible)
|
|
if outer != nil {
|
|
terminalOutput = hotPathCompatibilityOutput(outer, terminalOutput, protocol)
|
|
}
|
|
intent := hotPathTerminalIntent{Output: terminalOutput}
|
|
mapped, err := s.lightFlows.beginCleanupWithOuter(ctx, requestID, s.edgeIDValue(), intent, outer, s.requestCoordinator)
|
|
if err != nil {
|
|
return normalizedStageOutput{}, false, err
|
|
}
|
|
return mapped, true, nil
|
|
}
|
|
mapped, err := s.lightFlows.issueTools(ctx, requestID, s.edgeIDValue(), output, visible, kind, outer, s.requestCoordinator)
|
|
if err != nil {
|
|
return normalizedStageOutput{}, false, err
|
|
}
|
|
return mapped, true, nil
|
|
}
|
|
|
|
func classifyHotPathReviewOutput(requestID string, phase hotPathLightPhase, output normalizedStageOutput, evidence hotPathReviewEvidence) (hotPathPendingKind, bool, error) {
|
|
paths := newReservedPaths(requestID)
|
|
switch phase {
|
|
case hotPathPhaseReviewActive:
|
|
if len(output.ToolCalls) == 0 {
|
|
if !evidence.planRead || !evidence.reviewRead || !evidence.inspected {
|
|
return "", false, fmt.Errorf("review completion requires both artifact reads and a successful ordinary result inspection")
|
|
}
|
|
if strings.TrimSpace(output.Content) == "" {
|
|
return "", false, fmt.Errorf("review terminal output must be non-empty")
|
|
}
|
|
return "", true, nil
|
|
}
|
|
for _, call := range output.ToolCalls {
|
|
observed := reservedPathsFromToolCall(call)
|
|
for _, path := range observed {
|
|
clean := cleanRelativePath(path)
|
|
if clean != cleanRelativePath(paths.PlanPath) && clean != cleanRelativePath(paths.ReviewPath) {
|
|
return "", false, fmt.Errorf("review inspection targets an unissued artifact")
|
|
}
|
|
}
|
|
}
|
|
if evidence.planRead && evidence.reviewRead && evidence.inspected {
|
|
for _, call := range output.ToolCalls {
|
|
if len(reservedPathsFromToolCall(call)) > 0 {
|
|
return "", false, fmt.Errorf("review repair cannot restart artifact inspection")
|
|
}
|
|
}
|
|
return hotPathPendingReviewRepair, false, nil
|
|
}
|
|
return hotPathPendingReviewInspection, false, nil
|
|
|
|
case hotPathPhaseReviewRepair:
|
|
if len(output.ToolCalls) == 0 {
|
|
if !evidence.planRead || !evidence.reviewRead || !evidence.inspected {
|
|
return "", false, fmt.Errorf("repair completion requires retained review evidence")
|
|
}
|
|
if strings.TrimSpace(output.Content) == "" {
|
|
return "", false, fmt.Errorf("review terminal output must be non-empty")
|
|
}
|
|
return "", true, nil
|
|
}
|
|
for _, call := range output.ToolCalls {
|
|
if len(reservedPathsFromToolCall(call)) > 0 {
|
|
return "", false, fmt.Errorf("repair cannot start a second review cycle")
|
|
}
|
|
}
|
|
return hotPathPendingReviewRepair, false, nil
|
|
|
|
default:
|
|
return "", false, fmt.Errorf("phase %q is not a review phase", phase)
|
|
}
|
|
}
|