iop/apps/edge/internal/openai/hot_path_review.go
toki c9939edf24 fix(edge): caller workspace 하이브리드 경계를 복구한다
실제 provider 요청과 작업 공간 handoff가 같은 PLAN/REVIEW 계약을 사용해야 하며, 단계별 prepare와 pair-write 의미가 테스트와 런타임에서 일치해야 한다.
2026-08-15 04:01:21 +09:00

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)
}
}