Files
tutti/services/tuttid/api/daemon_agent_side_conversations.go
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

357 lines
15 KiB
Go

package api
import (
"context"
"errors"
agenthost "github.com/tutti-os/tutti/packages/agent/host"
tuttigenerated "github.com/tutti-os/tutti/services/tuttid/api/generated"
"github.com/tutti-os/tutti/services/tuttid/apierrors"
agentservice "github.com/tutti-os/tutti/services/tuttid/service/agent"
)
type SideConversationService interface {
ResolveSideConversation(context.Context, string, string) (agenthost.SideConversationCapabilities, error)
OpenSideConversation(context.Context, string, string, agentservice.OpenSideConversationInput) (agentservice.SideConversation, error)
SendSideConversation(context.Context, string, string, agentservice.SendSideConversationInput) (agenthost.RuntimeExecResult, error)
CancelSideConversation(context.Context, string, string, string) (agenthost.RuntimeCancelResult, error)
SubmitSideConversationInteractive(context.Context, agenthost.RuntimeSubmitInteractiveInput) (agenthost.RuntimeSubmitInteractiveResult, error)
CloseSideConversation(context.Context, string, string) error
}
func generatedSideCapabilities(
input agenthost.SideConversationCapabilities,
) tuttigenerated.WorkspaceAgentSideCapabilities {
return tuttigenerated.WorkspaceAgentSideCapabilities{
Supported: input.Supported, ActiveSourceTurn: input.ActiveSourceTurn,
Ephemeral: input.Ephemeral, HideInheritedTurns: input.HideInheritedTurns,
ModelBoundaryInjected: input.ModelBoundaryInjected,
}
}
func generatedSideConversation(
input agentservice.SideConversation,
) tuttigenerated.WorkspaceAgentSideConversation {
status := input.Status
if status == "" {
status = "idle"
}
return tuttigenerated.WorkspaceAgentSideConversation{
WorkspaceId: input.WorkspaceID, SourceAgentSessionId: input.SourceAgentSessionID,
SideAgentSessionId: input.SideAgentSessionID, Provider: input.Provider,
Status: status, Capabilities: generatedSideCapabilities(input.Capabilities),
}
}
func sideConflictError(err error) tuttigenerated.ApiErrorResponse {
reason := "agent_side_conversation_conflict"
switch {
case errors.Is(err, agentservice.ErrSideConversationUnsupported):
reason = "agent_side_conversation_unsupported"
case errors.Is(err, agentservice.ErrSideConversationInProgress):
reason = "agent_side_conversation_in_progress"
case errors.Is(err, agentservice.ErrSideConversationExpired):
reason = "agent_side_conversation_expired"
case errors.Is(err, agentservice.ErrRuntimeSessionDisconnected):
reason = "agent_side_conversation_expired"
case errors.Is(err, agenthost.ErrInteractiveRequestNotLive):
reason = "agent_side_interaction_not_live"
case errors.Is(err, agenthost.ErrInteractiveAlreadyAnswered):
reason = "agent_side_interaction_already_answered"
}
return protocolErrorResponse(apierrors.New(
apierrors.StatusWorkspaceIssueExists,
tuttigenerated.WorkspaceOperationFailed,
reason,
apierrors.WithCause(err),
))
}
func isSideConflict(err error) bool {
return errors.Is(err, agentservice.ErrSideConversationUnsupported) ||
errors.Is(err, agentservice.ErrSideConversationInProgress) ||
errors.Is(err, agentservice.ErrSideConversationConflict) ||
errors.Is(err, agentservice.ErrSideConversationExpired) ||
errors.Is(err, agentservice.ErrRuntimeSessionDisconnected) ||
errors.Is(err, agenthost.ErrInteractiveRequestNotLive) ||
errors.Is(err, agenthost.ErrInteractiveAlreadyAnswered)
}
func (api DaemonAPI) ResolveWorkspaceAgentSideCapabilities(
ctx context.Context,
request tuttigenerated.ResolveWorkspaceAgentSideCapabilitiesRequestObject,
) (tuttigenerated.ResolveWorkspaceAgentSideCapabilitiesResponseObject, error) {
if api.SideConversationService == nil {
return tuttigenerated.ResolveWorkspaceAgentSideCapabilities503JSONResponse{
ServiceUnavailableErrorJSONResponse: agentSessionServiceUnavailableError(),
}, nil
}
if !api.agentSideConversationEnabled(ctx) {
return tuttigenerated.ResolveWorkspaceAgentSideCapabilities200JSONResponse{
Capabilities: generatedSideCapabilities(agenthost.SideConversationCapabilities{}),
}, nil
}
capabilities, err := api.SideConversationService.ResolveSideConversation(
ctx, string(request.WorkspaceID), string(request.AgentSessionID),
)
if err != nil {
if isSideConflict(err) {
return tuttigenerated.ResolveWorkspaceAgentSideCapabilities409JSONResponse(
sideConflictError(err),
), nil
}
protocolErr := apierrors.Classify(err)
if protocolErr.Code == tuttigenerated.InvalidRequest {
return tuttigenerated.ResolveWorkspaceAgentSideCapabilities400JSONResponse{
InvalidRequestErrorJSONResponse: invalidRequestError(protocolErr),
}, nil
}
if protocolErr.Code == tuttigenerated.WorkspaceNotFound {
return tuttigenerated.ResolveWorkspaceAgentSideCapabilities404JSONResponse{
WorkspaceNotFoundErrorJSONResponse: workspaceNotFoundError(protocolErr),
}, nil
}
return tuttigenerated.ResolveWorkspaceAgentSideCapabilities502JSONResponse{
WorkspaceOperationErrorJSONResponse: workspaceOperationError(protocolErr),
}, nil
}
return tuttigenerated.ResolveWorkspaceAgentSideCapabilities200JSONResponse{
Capabilities: generatedSideCapabilities(capabilities),
}, nil
}
func (api DaemonAPI) OpenWorkspaceAgentSideConversation(
ctx context.Context,
request tuttigenerated.OpenWorkspaceAgentSideConversationRequestObject,
) (tuttigenerated.OpenWorkspaceAgentSideConversationResponseObject, error) {
if api.SideConversationService == nil {
return tuttigenerated.OpenWorkspaceAgentSideConversation503JSONResponse{
ServiceUnavailableErrorJSONResponse: agentSessionServiceUnavailableError(),
}, nil
}
if !api.agentSideConversationEnabled(ctx) {
return tuttigenerated.OpenWorkspaceAgentSideConversation400JSONResponse{
InvalidRequestErrorJSONResponse: agentSideConversationDisabledError(),
}, nil
}
if request.Body == nil {
return tuttigenerated.OpenWorkspaceAgentSideConversation400JSONResponse{
InvalidRequestErrorJSONResponse: invalidRequestError(
apierrors.EmptyBody(apierrors.WithDeveloperMessage("empty body")),
),
}, nil
}
side, err := api.SideConversationService.OpenSideConversation(
ctx, string(request.WorkspaceID), string(request.AgentSessionID),
agentservice.OpenSideConversationInput{
SideAgentSessionID: request.Body.SideAgentSessionId,
RequestID: request.Body.RequestId,
},
)
if err != nil {
if isSideConflict(err) {
return tuttigenerated.OpenWorkspaceAgentSideConversation409JSONResponse(
sideConflictError(err),
), nil
}
protocolErr := apierrors.Classify(err)
if protocolErr.Code == tuttigenerated.InvalidRequest {
return tuttigenerated.OpenWorkspaceAgentSideConversation400JSONResponse{
InvalidRequestErrorJSONResponse: invalidRequestError(protocolErr),
}, nil
}
if protocolErr.Code == tuttigenerated.WorkspaceNotFound {
return tuttigenerated.OpenWorkspaceAgentSideConversation404JSONResponse{
WorkspaceNotFoundErrorJSONResponse: workspaceNotFoundError(protocolErr),
}, nil
}
return tuttigenerated.OpenWorkspaceAgentSideConversation502JSONResponse{
WorkspaceOperationErrorJSONResponse: workspaceOperationError(protocolErr),
}, nil
}
return tuttigenerated.OpenWorkspaceAgentSideConversation200JSONResponse{
Side: generatedSideConversation(side),
}, nil
}
func (api DaemonAPI) SendWorkspaceAgentSideConversationInput(
ctx context.Context,
request tuttigenerated.SendWorkspaceAgentSideConversationInputRequestObject,
) (tuttigenerated.SendWorkspaceAgentSideConversationInputResponseObject, error) {
if api.SideConversationService == nil {
return tuttigenerated.SendWorkspaceAgentSideConversationInput503JSONResponse{
ServiceUnavailableErrorJSONResponse: agentSessionServiceUnavailableError(),
}, nil
}
if request.Body == nil {
return tuttigenerated.SendWorkspaceAgentSideConversationInput400JSONResponse{
InvalidRequestErrorJSONResponse: invalidRequestError(
apierrors.EmptyBody(apierrors.WithDeveloperMessage("empty body")),
),
}, nil
}
result, err := api.SideConversationService.SendSideConversation(
ctx, string(request.WorkspaceID), request.SideAgentSessionID,
agentservice.SendSideConversationInput{
TurnID: request.Body.TurnId, ClientSubmitID: request.Body.ClientSubmitId,
Content: agentPromptContentFromGenerated(request.Body.Content),
DisplayPrompt: stringPtrValue(request.Body.DisplayPrompt),
},
)
if err != nil {
if isSideConflict(err) {
return tuttigenerated.SendWorkspaceAgentSideConversationInput409JSONResponse(
sideConflictError(err),
), nil
}
protocolErr := apierrors.Classify(err)
if protocolErr.Code == tuttigenerated.InvalidRequest {
return tuttigenerated.SendWorkspaceAgentSideConversationInput400JSONResponse{
InvalidRequestErrorJSONResponse: invalidRequestError(protocolErr),
}, nil
}
if protocolErr.Code == tuttigenerated.WorkspaceNotFound {
return tuttigenerated.SendWorkspaceAgentSideConversationInput404JSONResponse{
WorkspaceNotFoundErrorJSONResponse: workspaceNotFoundError(protocolErr),
}, nil
}
return tuttigenerated.SendWorkspaceAgentSideConversationInput502JSONResponse{
WorkspaceOperationErrorJSONResponse: workspaceOperationError(protocolErr),
}, nil
}
return tuttigenerated.SendWorkspaceAgentSideConversationInput200JSONResponse{
SideAgentSessionId: result.AgentSessionID, TurnId: result.TurnID,
Accepted: result.Accepted, Status: result.Status,
}, nil
}
func (api DaemonAPI) CancelWorkspaceAgentSideConversationTurn(
ctx context.Context,
request tuttigenerated.CancelWorkspaceAgentSideConversationTurnRequestObject,
) (tuttigenerated.CancelWorkspaceAgentSideConversationTurnResponseObject, error) {
if api.SideConversationService == nil {
return tuttigenerated.CancelWorkspaceAgentSideConversationTurn503JSONResponse{
ServiceUnavailableErrorJSONResponse: agentSessionServiceUnavailableError(),
}, nil
}
result, err := api.SideConversationService.CancelSideConversation(
ctx, string(request.WorkspaceID), request.SideAgentSessionID, string(request.TurnID),
)
if err != nil {
if isSideConflict(err) {
return tuttigenerated.CancelWorkspaceAgentSideConversationTurn409JSONResponse(
sideConflictError(err),
), nil
}
protocolErr := apierrors.Classify(err)
if protocolErr.Code == tuttigenerated.InvalidRequest {
return tuttigenerated.CancelWorkspaceAgentSideConversationTurn400JSONResponse{
InvalidRequestErrorJSONResponse: invalidRequestError(protocolErr),
}, nil
}
if protocolErr.Code == tuttigenerated.WorkspaceNotFound {
return tuttigenerated.CancelWorkspaceAgentSideConversationTurn404JSONResponse{
WorkspaceNotFoundErrorJSONResponse: workspaceNotFoundError(protocolErr),
}, nil
}
return tuttigenerated.CancelWorkspaceAgentSideConversationTurn502JSONResponse{
WorkspaceOperationErrorJSONResponse: workspaceOperationError(protocolErr),
}, nil
}
return tuttigenerated.CancelWorkspaceAgentSideConversationTurn200JSONResponse{
Canceled: result.Canceled, TargetAbsent: result.TargetAbsent,
}, nil
}
func (api DaemonAPI) SubmitWorkspaceAgentSideConversationInteractive(
ctx context.Context,
request tuttigenerated.SubmitWorkspaceAgentSideConversationInteractiveRequestObject,
) (tuttigenerated.SubmitWorkspaceAgentSideConversationInteractiveResponseObject, error) {
if api.SideConversationService == nil {
return tuttigenerated.SubmitWorkspaceAgentSideConversationInteractive503JSONResponse{
ServiceUnavailableErrorJSONResponse: agentSessionServiceUnavailableError(),
}, nil
}
if request.Body == nil {
return tuttigenerated.SubmitWorkspaceAgentSideConversationInteractive400JSONResponse{
InvalidRequestErrorJSONResponse: invalidRequestError(
apierrors.EmptyBody(apierrors.WithDeveloperMessage("empty body")),
),
}, nil
}
result, err := api.SideConversationService.SubmitSideConversationInteractive(
ctx,
agenthost.RuntimeSubmitInteractiveInput{
WorkspaceID: string(request.WorkspaceID), RootAgentSessionID: request.SideAgentSessionID,
AgentSessionID: request.SideAgentSessionID, TurnID: string(request.TurnID),
RequestID: string(request.RequestID), Action: stringPtrValue(request.Body.Action),
OptionID: stringPtrValue(request.Body.OptionId), Payload: optionalPayloadMap(request.Body.Payload),
},
)
if err != nil {
if isSideConflict(err) {
return tuttigenerated.SubmitWorkspaceAgentSideConversationInteractive409JSONResponse(
sideConflictError(err),
), nil
}
if errors.Is(err, agenthost.ErrInteractiveResponseInvalid) {
return tuttigenerated.SubmitWorkspaceAgentSideConversationInteractive400JSONResponse{
InvalidRequestErrorJSONResponse: invalidRequestError(
apierrors.InvalidRequest(
"agent_side_interaction_invalid_response",
apierrors.WithCause(err),
),
),
}, nil
}
protocolErr := apierrors.Classify(err)
if protocolErr.Code == tuttigenerated.InvalidRequest {
return tuttigenerated.SubmitWorkspaceAgentSideConversationInteractive400JSONResponse{
InvalidRequestErrorJSONResponse: invalidRequestError(protocolErr),
}, nil
}
if protocolErr.Code == tuttigenerated.WorkspaceNotFound {
return tuttigenerated.SubmitWorkspaceAgentSideConversationInteractive404JSONResponse{
WorkspaceNotFoundErrorJSONResponse: workspaceNotFoundError(protocolErr),
}, nil
}
return tuttigenerated.SubmitWorkspaceAgentSideConversationInteractive502JSONResponse{
WorkspaceOperationErrorJSONResponse: workspaceOperationError(protocolErr),
}, nil
}
return tuttigenerated.SubmitWorkspaceAgentSideConversationInteractive200JSONResponse{
Disposition: string(result.Disposition),
}, nil
}
func (api DaemonAPI) CloseWorkspaceAgentSideConversation(
ctx context.Context,
request tuttigenerated.CloseWorkspaceAgentSideConversationRequestObject,
) (tuttigenerated.CloseWorkspaceAgentSideConversationResponseObject, error) {
if api.SideConversationService == nil {
return tuttigenerated.CloseWorkspaceAgentSideConversation503JSONResponse{
ServiceUnavailableErrorJSONResponse: agentSessionServiceUnavailableError(),
}, nil
}
err := api.SideConversationService.CloseSideConversation(
ctx, string(request.WorkspaceID), request.SideAgentSessionID,
)
if err != nil && !errors.Is(err, agentservice.ErrSideConversationExpired) {
protocolErr := apierrors.Classify(err)
if protocolErr.Code == tuttigenerated.InvalidRequest {
return tuttigenerated.CloseWorkspaceAgentSideConversation400JSONResponse{
InvalidRequestErrorJSONResponse: invalidRequestError(protocolErr),
}, nil
}
if protocolErr.Code == tuttigenerated.WorkspaceNotFound {
return tuttigenerated.CloseWorkspaceAgentSideConversation404JSONResponse{
WorkspaceNotFoundErrorJSONResponse: workspaceNotFoundError(protocolErr),
}, nil
}
return tuttigenerated.CloseWorkspaceAgentSideConversation502JSONResponse{
WorkspaceOperationErrorJSONResponse: workspaceOperationError(protocolErr),
}, nil
}
return tuttigenerated.CloseWorkspaceAgentSideConversation204Response{}, nil
}