Claude의 단일 Anthropic 요청 안에서 IOP가 Plan, Work, Review와 workspace 도구 실행을 끝내고 실제 dev smoke로 계약을 검증할 수 있어야 한다.\n\n완료 task evidence와 마일스톤 검토 상태도 같은 변경에 고정한다.
297 lines
9.8 KiB
Go
297 lines
9.8 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
|
|
iop "iop/proto/gen/iop"
|
|
)
|
|
|
|
var (
|
|
ErrSingleRequestInternalArtifactUnavailable = errors.New("single-request internal artifact is unavailable")
|
|
ErrSingleRequestInternalArtifactInvalid = errors.New("single-request internal artifact request is invalid")
|
|
ErrSingleRequestInternalArtifactBudget = errors.New("single-request internal artifact budget exceeded")
|
|
ErrSingleRequestInternalArtifactFailed = errors.New("single-request internal artifact operation failed")
|
|
ErrSingleRequestWorkspaceOpen = errors.New("single-request workspace open failed")
|
|
)
|
|
|
|
// SingleRequestArtifactKind is the coordinator-facing closed artifact set. It
|
|
// deliberately carries no filename or relative path.
|
|
type SingleRequestArtifactKind string
|
|
|
|
const (
|
|
SingleRequestArtifactPlan SingleRequestArtifactKind = "plan"
|
|
SingleRequestArtifactReview SingleRequestArtifactKind = "review"
|
|
)
|
|
|
|
type singleRequestWorkspaceArtifactRuntime interface {
|
|
workspaceArtifact(context.Context, *SingleRequestWorkspaceBinding, *iop.WorkspaceArtifactRequest, int) (*iop.WorkspaceArtifactResponse, error)
|
|
}
|
|
|
|
type singleRequestArtifactOperation struct {
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
runtime singleRequestWorkspaceArtifactRuntime
|
|
openRuntime singleRequestWorkspaceToolRuntime
|
|
binding *SingleRequestWorkspaceBinding
|
|
kind iop.WorkspaceArtifactKind
|
|
maximum int
|
|
complete func()
|
|
}
|
|
|
|
func (h *singleRequestHandle) ReadInternalArtifact(ctx context.Context, kind SingleRequestArtifactKind) ([]byte, error) {
|
|
operation, err := h.prepareSingleRequestArtifact(ctx, kind, 0)
|
|
if err != nil {
|
|
h.failSingleRequestArtifact(ctx, err)
|
|
return nil, err
|
|
}
|
|
defer operation.cancel()
|
|
defer operation.complete()
|
|
|
|
if err := operation.ctx.Err(); err != nil {
|
|
return nil, h.finishSingleRequestArtifactFailure(operation.ctx, err)
|
|
}
|
|
if err := h.ensureSingleRequestWorkspaceOpen(operation.ctx, operation.openRuntime, operation.binding); err != nil {
|
|
return nil, h.finishSingleRequestArtifactFailure(operation.ctx, err)
|
|
}
|
|
if err := operation.ctx.Err(); err != nil {
|
|
return nil, h.finishSingleRequestArtifactFailure(operation.ctx, err)
|
|
}
|
|
response, err := operation.runtime.workspaceArtifact(operation.ctx, operation.binding, &iop.WorkspaceArtifactRequest{
|
|
RequestId: h.req.RequestID,
|
|
Kind: operation.kind,
|
|
Operation: iop.WorkspaceArtifactOperation_WORKSPACE_ARTIFACT_OPERATION_READ,
|
|
}, operation.maximum)
|
|
if err != nil || response.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
return nil, h.finishSingleRequestArtifactFailure(operation.ctx, err)
|
|
}
|
|
if err := operation.ctx.Err(); err != nil {
|
|
return nil, h.finishSingleRequestArtifactFailure(operation.ctx, err)
|
|
}
|
|
return append([]byte(nil), response.GetContent()...), nil
|
|
}
|
|
|
|
func (h *singleRequestHandle) WriteInternalArtifact(ctx context.Context, kind SingleRequestArtifactKind, content []byte) error {
|
|
operation, err := h.prepareSingleRequestArtifact(ctx, kind, len(content))
|
|
if err != nil {
|
|
h.failSingleRequestArtifact(ctx, err)
|
|
return err
|
|
}
|
|
defer operation.cancel()
|
|
defer operation.complete()
|
|
|
|
if err := operation.ctx.Err(); err != nil {
|
|
return h.finishSingleRequestArtifactFailure(operation.ctx, err)
|
|
}
|
|
if err := h.ensureSingleRequestWorkspaceOpen(operation.ctx, operation.openRuntime, operation.binding); err != nil {
|
|
return h.finishSingleRequestArtifactFailure(operation.ctx, err)
|
|
}
|
|
if err := operation.ctx.Err(); err != nil {
|
|
return h.finishSingleRequestArtifactFailure(operation.ctx, err)
|
|
}
|
|
response, err := operation.runtime.workspaceArtifact(operation.ctx, operation.binding, &iop.WorkspaceArtifactRequest{
|
|
RequestId: h.req.RequestID,
|
|
Kind: operation.kind,
|
|
Operation: iop.WorkspaceArtifactOperation_WORKSPACE_ARTIFACT_OPERATION_WRITE,
|
|
Content: append([]byte(nil), content...),
|
|
}, operation.maximum)
|
|
if err != nil || response.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
return h.finishSingleRequestArtifactFailure(operation.ctx, err)
|
|
}
|
|
if err := operation.ctx.Err(); err != nil {
|
|
return h.finishSingleRequestArtifactFailure(operation.ctx, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (h *singleRequestHandle) prepareSingleRequestArtifact(ctx context.Context, kind SingleRequestArtifactKind, contentBytes int) (*singleRequestArtifactOperation, error) {
|
|
if ctx == nil {
|
|
return nil, ErrSingleRequestInternalArtifactInvalid
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
protoKind, ok := singleRequestArtifactProtoKind(kind)
|
|
if !ok {
|
|
return nil, ErrSingleRequestInternalArtifactInvalid
|
|
}
|
|
|
|
h.mu.Lock()
|
|
if isTerminalState(h.state) || h.state == SingleRequestStateFinalizing {
|
|
h.mu.Unlock()
|
|
return nil, ErrSingleRequestTerminal
|
|
}
|
|
if h.state == SingleRequestStateInternalTool || h.activeStageIDLocked() == "" || h.toolLoop.stageDeadline.IsZero() {
|
|
h.mu.Unlock()
|
|
return nil, ErrSingleRequestInternalArtifactInvalid
|
|
}
|
|
openRuntime := h.toolLoop.runtime
|
|
runtime, ok := openRuntime.(singleRequestWorkspaceArtifactRuntime)
|
|
binding := h.binding.Workspace.Clone()
|
|
deadline := h.toolLoop.stageDeadline
|
|
if !ok || runtime == nil || binding == nil {
|
|
h.mu.Unlock()
|
|
return nil, ErrSingleRequestInternalArtifactUnavailable
|
|
}
|
|
maximum := binding.Limits.MaxOutputBytes
|
|
if maximum < 1 || contentBytes < 0 || contentBytes > maximum {
|
|
h.mu.Unlock()
|
|
return nil, ErrSingleRequestInternalArtifactBudget
|
|
}
|
|
if !time.Now().Before(deadline) {
|
|
h.mu.Unlock()
|
|
return nil, ErrSingleRequestInternalArtifactBudget
|
|
}
|
|
h.toolWg.Add(1)
|
|
h.toolWork++
|
|
h.mu.Unlock()
|
|
|
|
operationCtx, cancel := singleRequestArtifactContext(h.execCtx, ctx, deadline)
|
|
return &singleRequestArtifactOperation{
|
|
ctx: operationCtx, cancel: cancel, runtime: runtime, openRuntime: openRuntime, binding: binding,
|
|
kind: protoKind, maximum: maximum,
|
|
complete: func() {
|
|
h.mu.Lock()
|
|
h.toolWork--
|
|
h.mu.Unlock()
|
|
h.toolWg.Done()
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func singleRequestArtifactProtoKind(kind SingleRequestArtifactKind) (iop.WorkspaceArtifactKind, bool) {
|
|
switch kind {
|
|
case SingleRequestArtifactPlan:
|
|
return iop.WorkspaceArtifactKind_WORKSPACE_ARTIFACT_KIND_PLAN, true
|
|
case SingleRequestArtifactReview:
|
|
return iop.WorkspaceArtifactKind_WORKSPACE_ARTIFACT_KIND_REVIEW, true
|
|
default:
|
|
return iop.WorkspaceArtifactKind_WORKSPACE_ARTIFACT_KIND_UNSPECIFIED, false
|
|
}
|
|
}
|
|
|
|
func singleRequestArtifactContext(execution, caller context.Context, stageDeadline time.Time) (context.Context, context.CancelFunc) {
|
|
deadline := stageDeadline
|
|
if callerDeadline, ok := caller.Deadline(); ok && callerDeadline.Before(deadline) {
|
|
deadline = callerDeadline
|
|
}
|
|
ctx, cancel := context.WithDeadline(execution, deadline)
|
|
stop := context.AfterFunc(caller, cancel)
|
|
return ctx, func() {
|
|
stop()
|
|
cancel()
|
|
}
|
|
}
|
|
|
|
func (h *singleRequestHandle) finishSingleRequestArtifactFailure(ctx context.Context, _ error) error {
|
|
err := ErrSingleRequestInternalArtifactFailed
|
|
if ctx != nil && ctx.Err() != nil {
|
|
err = ctx.Err()
|
|
}
|
|
h.failSingleRequestArtifact(ctx, err)
|
|
return err
|
|
}
|
|
|
|
func (h *singleRequestHandle) failSingleRequestArtifact(ctx context.Context, err error) {
|
|
fallback := singleRequestErrorClassFromErr(err)
|
|
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, ErrSingleRequestInternalArtifactBudget) {
|
|
fallback = singleRequestErrorClassTimeout
|
|
}
|
|
outcome, errorClass := h.classifyChildOperationContext(ctx, fallback)
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
if isTerminalState(h.state) || h.state == SingleRequestStateFinalizing {
|
|
return
|
|
}
|
|
if outcome == singleRequestOutcomeCancel {
|
|
h.cancelLocked()
|
|
return
|
|
}
|
|
if errorClass == singleRequestErrorClassInternalToolBudget {
|
|
h.failLockedWithErrorClass(ErrSingleRequestInternalToolBudget, singleRequestErrorClassInternalToolBudget)
|
|
return
|
|
}
|
|
if errorClass == singleRequestErrorClassTimeout {
|
|
h.failLockedWithErrorClass(ErrSingleRequestInternalArtifactBudget, singleRequestErrorClassTimeout)
|
|
return
|
|
}
|
|
h.failLockedWithErrorClass(err, errorClass)
|
|
}
|
|
|
|
// ensureSingleRequestWorkspaceOpen serializes the one permitted open attempt
|
|
// across model-tool and internal-artifact callers. A failed attempt is cached so
|
|
// no racing caller can silently send a second open on the same request.
|
|
func (h *singleRequestHandle) ensureSingleRequestWorkspaceOpen(ctx context.Context, runtime singleRequestWorkspaceToolRuntime, binding *SingleRequestWorkspaceBinding) error {
|
|
for {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
h.mu.Lock()
|
|
if h.toolLoop.opened {
|
|
h.mu.Unlock()
|
|
return nil
|
|
}
|
|
if h.toolLoop.opening {
|
|
done := h.toolLoop.openDone
|
|
h.mu.Unlock()
|
|
select {
|
|
case <-done:
|
|
continue
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
if h.toolLoop.openAttempted {
|
|
err := h.toolLoop.openErr
|
|
if err == nil {
|
|
err = ErrSingleRequestWorkspaceOpen
|
|
}
|
|
h.mu.Unlock()
|
|
return err
|
|
}
|
|
if runtime == nil || binding == nil {
|
|
h.mu.Unlock()
|
|
return ErrSingleRequestWorkspaceOpen
|
|
}
|
|
h.toolLoop.openAttempted = true
|
|
h.toolLoop.opening = true
|
|
h.toolLoop.openDone = make(chan struct{})
|
|
done := h.toolLoop.openDone
|
|
h.mu.Unlock()
|
|
|
|
response, err := runtime.workspaceOpen(ctx, binding, &iop.WorkspaceOpenRequest{
|
|
RequestId: h.req.RequestID, WorkspaceRef: binding.Ref,
|
|
TimeoutMs: singleRequestContextRemainingMilliseconds(ctx),
|
|
})
|
|
accepted := err == nil && response.GetStatus() == iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS
|
|
h.mu.Lock()
|
|
if accepted {
|
|
h.toolLoop.opened = true
|
|
h.toolLoop.openErr = nil
|
|
} else {
|
|
h.toolLoop.openErr = ErrSingleRequestWorkspaceOpen
|
|
}
|
|
h.toolLoop.opening = false
|
|
close(done)
|
|
h.mu.Unlock()
|
|
if !accepted {
|
|
if ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
return ErrSingleRequestWorkspaceOpen
|
|
}
|
|
if ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func singleRequestContextRemainingMilliseconds(ctx context.Context) int64 {
|
|
deadline, ok := ctx.Deadline()
|
|
if !ok {
|
|
return 0
|
|
}
|
|
return internalToolRemainingMilliseconds(deadline)
|
|
}
|