Claude의 단일 Anthropic 요청 안에서 IOP가 Plan, Work, Review와 workspace 도구 실행을 끝내고 실제 dev smoke로 계약을 검증할 수 있어야 한다.\n\n완료 task evidence와 마일스톤 검토 상태도 같은 변경에 고정한다.
196 lines
6.7 KiB
Go
196 lines
6.7 KiB
Go
package openai
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
"time"
|
|
|
|
edgeservice "iop/apps/edge/internal/service"
|
|
)
|
|
|
|
// SingleRequestExecutor is the concurrent, request-safe composite executor
|
|
// driving the Plan -> Work -> Review lifecycle behind the single-request interface.
|
|
type SingleRequestExecutor struct {
|
|
provider *singleRequestProviderStage
|
|
bridge *singleRequestWorkToolBridge
|
|
plan *singleRequestPlanStage
|
|
work *singleRequestWorkStage
|
|
review *singleRequestReviewStage
|
|
}
|
|
|
|
// NewSingleRequestExecutor constructs a production composite single-request executor
|
|
// backed by private stage drivers and the correlated continuation bridge.
|
|
func NewSingleRequestExecutor(service edgeserviceRunner) *SingleRequestExecutor {
|
|
provider := newSingleRequestProviderStage(service)
|
|
bridge := newSingleRequestWorkToolBridge()
|
|
return &SingleRequestExecutor{
|
|
provider: provider,
|
|
bridge: bridge,
|
|
plan: newSingleRequestPlanStage(provider),
|
|
work: newSingleRequestWorkStage(provider, bridge),
|
|
review: newSingleRequestReviewStage(provider, bridge),
|
|
}
|
|
}
|
|
|
|
// ExecuteSingleRequest executes Plan -> Work -> Review sequentially for one request
|
|
// against a single controller and immutable binding.
|
|
func (s *SingleRequestExecutor) ExecuteSingleRequest(ctx context.Context, req edgeservice.SingleRequestRequest, ctrl edgeservice.SingleRequestController) error {
|
|
if s == nil || s.provider == nil || s.bridge == nil || s.plan == nil || s.work == nil || s.review == nil || ctrl == nil {
|
|
return edgeservice.ErrSingleRequestExecutorUnavailable
|
|
}
|
|
if req.RequestID == "" || req.RequestID != ctrl.RequestID() {
|
|
return edgeservice.ErrSingleRequestIdentityMismatch
|
|
}
|
|
binding := ctrl.Binding()
|
|
if binding == nil || req.Binding == nil {
|
|
return edgeservice.ErrSingleRequestInvalidBinding
|
|
}
|
|
if binding.Workspace == nil || binding.Workspace.NodeID == "" {
|
|
return edgeservice.ErrSingleRequestInvalidBinding
|
|
}
|
|
|
|
defer s.bridge.clearRequest(req.RequestID)
|
|
quality := newSingleRequestQualityGate()
|
|
|
|
seqCtrl := &singleRequestSequenceController{
|
|
SingleRequestController: ctrl,
|
|
}
|
|
|
|
nodeRef := binding.Workspace.NodeID
|
|
|
|
// Stage 1: Plan
|
|
planReq := singleRequestPlanStageRequest{
|
|
RequestID: req.RequestID,
|
|
Task: req.Prompt,
|
|
StageBinding: binding.Plan,
|
|
Limits: binding.Limits,
|
|
NodeRef: nodeRef,
|
|
SessionID: req.RequestID,
|
|
UsageAttribution: singleRequestUsageAttribution(binding.Plan),
|
|
Sequence: 1,
|
|
Quality: quality,
|
|
}
|
|
_, err := s.plan.run(ctx, planReq, seqCtrl)
|
|
if err != nil {
|
|
return submitSingleRequestClosedTerminal(ctx, req.RequestID, seqCtrl, err)
|
|
}
|
|
|
|
// Stage 2: Work
|
|
workReq := singleRequestWorkStageRequest{
|
|
RequestID: req.RequestID,
|
|
Task: req.Prompt,
|
|
StageBinding: binding.Work,
|
|
Limits: binding.Limits,
|
|
NodeRef: nodeRef,
|
|
SessionID: req.RequestID,
|
|
UsageAttribution: singleRequestUsageAttribution(binding.Work),
|
|
Sequence: 1,
|
|
Quality: quality,
|
|
}
|
|
workResult, err := s.work.run(ctx, workReq, seqCtrl)
|
|
if err != nil {
|
|
return submitSingleRequestClosedTerminal(ctx, req.RequestID, seqCtrl, err)
|
|
}
|
|
|
|
// Stage 3: Review
|
|
reviewReq := singleRequestReviewStageRequest{
|
|
RequestID: req.RequestID,
|
|
Task: req.Prompt,
|
|
Work: workResult,
|
|
StageBinding: binding.Review,
|
|
Limits: binding.Limits,
|
|
NodeRef: nodeRef,
|
|
SessionID: req.RequestID,
|
|
UsageAttribution: singleRequestUsageAttribution(binding.Review),
|
|
Sequence: 1,
|
|
Quality: quality,
|
|
}
|
|
_, err = s.review.run(ctx, reviewReq, seqCtrl)
|
|
if err != nil {
|
|
return submitSingleRequestClosedTerminal(ctx, req.RequestID, seqCtrl, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func submitSingleRequestClosedTerminal(ctx context.Context, requestID string, ctrl edgeservice.SingleRequestController, stageErr error) error {
|
|
// The parent request context is owned by the service. Returning its error
|
|
// lets that owner distinguish caller cancellation from wall-clock budget
|
|
// exhaustion without racing a stage terminal envelope.
|
|
if ctx != nil {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if deadline, ok := ctx.Deadline(); ok && !time.Now().Before(deadline) {
|
|
return context.DeadlineExceeded
|
|
}
|
|
}
|
|
disposition, ok := singleRequestTerminalDisposition(stageErr)
|
|
if !ok {
|
|
switch {
|
|
case errors.Is(stageErr, context.DeadlineExceeded):
|
|
disposition = edgeservice.SingleRequestTerminalDisposition{Kind: edgeservice.SingleRequestTerminalError, ErrorClass: edgeservice.SingleRequestTerminalErrorTimeout}
|
|
default:
|
|
disposition = edgeservice.SingleRequestTerminalDisposition{Kind: edgeservice.SingleRequestTerminalError, ErrorClass: edgeservice.SingleRequestTerminalErrorProvider}
|
|
}
|
|
}
|
|
|
|
var envelope edgeservice.SingleRequestEnvelope
|
|
switch disposition.Kind {
|
|
case edgeservice.SingleRequestTerminalLength:
|
|
envelope = edgeservice.SingleRequestEnvelope{
|
|
RequestID: requestID,
|
|
Stage: edgeservice.SingleRequestStateFinalizing,
|
|
Result: &edgeservice.SingleRequestResult{
|
|
Terminal: disposition,
|
|
},
|
|
}
|
|
case edgeservice.SingleRequestTerminalCancelled:
|
|
envelope = edgeservice.SingleRequestEnvelope{RequestID: requestID, Stage: edgeservice.SingleRequestStateCancelled, Terminal: &disposition}
|
|
default:
|
|
if disposition.Kind != edgeservice.SingleRequestTerminalError || disposition.Validate() != nil {
|
|
disposition = edgeservice.SingleRequestTerminalDisposition{Kind: edgeservice.SingleRequestTerminalError, ErrorClass: edgeservice.SingleRequestTerminalErrorProvider}
|
|
}
|
|
envelope = edgeservice.SingleRequestEnvelope{
|
|
RequestID: requestID,
|
|
Stage: edgeservice.SingleRequestStateFailed,
|
|
Terminal: &disposition,
|
|
Err: edgeservice.ErrSingleRequestFailed,
|
|
}
|
|
}
|
|
if err := ctrl.SubmitEnvelope(envelope); err != nil && !errors.Is(err, edgeservice.ErrSingleRequestTerminal) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ContinueInternalTool delegates correlated tool continuation results to the
|
|
// request-safe bridge.
|
|
func (s *SingleRequestExecutor) ContinueInternalTool(ctx context.Context, result edgeservice.InternalWorkspaceToolResult) error {
|
|
if s == nil || s.bridge == nil {
|
|
return edgeservice.ErrSingleRequestExecutorUnavailable
|
|
}
|
|
return s.bridge.ContinueInternalTool(ctx, result)
|
|
}
|
|
|
|
type singleRequestSequenceController struct {
|
|
edgeservice.SingleRequestController
|
|
mu sync.Mutex
|
|
sequence uint64
|
|
}
|
|
|
|
func (c *singleRequestSequenceController) SubmitEnvelope(env edgeservice.SingleRequestEnvelope) error {
|
|
c.mu.Lock()
|
|
c.sequence++
|
|
env.Sequence = c.sequence
|
|
c.mu.Unlock()
|
|
return c.SingleRequestController.SubmitEnvelope(env)
|
|
}
|
|
|
|
func singleRequestUsageAttribution(binding edgeservice.SingleRequestStageBinding) string {
|
|
if binding.Dispatch != nil {
|
|
return binding.Dispatch.PrincipalRef
|
|
}
|
|
return ""
|
|
}
|