fix(openai): Review 읽기를 handoff 쓰기와 구분한다

Work 도구 호출에 review 경로가 포함됐다는 이유만으로 handoff write로 오분류하지 않는다. 실제 write content가 있는 호출만 handoff로 판정하고 단순 read/inspection은 caller 도구로 통과시킨다.
This commit is contained in:
toki 2026-08-15 08:42:16 +09:00
parent 64ab12b895
commit c051529c05
2 changed files with 35 additions and 16 deletions

View file

@ -981,29 +981,19 @@ func (s *hotPathLightStore) classifyLocalOutput(requestID, ownerEdgeID string, o
return "", nil
}
paths := newReservedPaths(requestID)
reviewWrites := 0
reviewWrites := make([]normalizedToolCall, 0, 1)
for _, call := range output.ToolCalls {
for _, observed := range reservedPathsFromToolCall(call) {
if cleanRelativePath(observed) == cleanRelativePath(paths.ReviewPath) {
reviewWrites++
}
if isWorkerReviewHandoffCall(record.binding, paths, call) {
reviewWrites = append(reviewWrites, call)
}
}
if reviewWrites == 0 {
if len(reviewWrites) == 0 {
return hotPathPendingLocalTools, nil
}
if reviewWrites != 1 || !record.localPlanRead {
if len(reviewWrites) != 1 || !record.localPlanRead {
return "", fmt.Errorf("worker review handoff must contain one exact review write after reading the plan")
}
var reviewCall normalizedToolCall
for _, call := range output.ToolCalls {
for _, observed := range reservedPathsFromToolCall(call) {
if cleanRelativePath(observed) == cleanRelativePath(paths.ReviewPath) {
reviewCall = call
}
}
}
content, err := artifactWriteContent(record.binding, reviewCall)
content, err := artifactWriteContent(record.binding, reviewWrites[0])
if err != nil {
return "", err
}
@ -1013,6 +1003,22 @@ func (s *hotPathLightStore) classifyLocalOutput(requestID, ownerEdgeID string, o
return hotPathPendingLocalHandoff, nil
}
func isWorkerReviewHandoffCall(binding *workspaceBinding, paths reservedPaths, call normalizedToolCall) bool {
write := binding.operation(opKindWrite)
if write == nil || strings.TrimSpace(write.contentField) == "" {
return false
}
if _, ok := lookupMappedArgument(call.Arguments, write.contentField); !ok {
return false
}
for _, observed := range reservedPathsFromToolCall(call) {
if cleanRelativePath(observed) == cleanRelativePath(paths.ReviewPath) {
return true
}
}
return false
}
type hotPathReviewEvidence struct {
planRead bool
reviewRead bool

View file

@ -78,6 +78,19 @@ func TestHotPathStageOrdinaryWorkspacePathPassesThrough(t *testing.T) {
}
}
func TestWorkerReviewReadIsNotClassifiedAsHandoffWrite(t *testing.T) {
binding := mustBinding(t, fullWorkspaceAlternative("workspace", "workspace", false), []any{openAIChatTool("workspace", structuredSchema())})
paths := newReservedPaths("req_review_read")
read := normalizedToolCall{Name: "workspace", Arguments: map[string]any{"path": paths.ReviewPath}}
if isWorkerReviewHandoffCall(binding, paths, read) {
t.Fatal("review read was classified as a handoff write")
}
write := normalizedToolCall{Name: "workspace", Arguments: map[string]any{"path": paths.ReviewPath, "content": testCompletedReviewText()}}
if !isWorkerReviewHandoffCall(binding, paths, write) {
t.Fatal("review write was not classified as a handoff write")
}
}
func TestWorkerProviderReceivesOnlyCallerTools(t *testing.T) {
callerTools := []any{openAIChatTool("run_command", commandSchema())}
tools := hotPathStageProviderTools(hotPathDispatchSnapshot{Phase: hotPathPhaseLocalActive, Tools: callerTools})