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
806 lines
35 KiB
Go
806 lines
35 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/tutti-os/tutti/packages/agent/daemon/providerregistry"
|
|
agenthost "github.com/tutti-os/tutti/packages/agent/host"
|
|
agentactivitybiz "github.com/tutti-os/tutti/packages/agent/store-sqlite"
|
|
"github.com/tutti-os/tutti/packages/agent/store-sqlite/canonical"
|
|
tuttigenerated "github.com/tutti-os/tutti/services/tuttid/api/generated"
|
|
"github.com/tutti-os/tutti/services/tuttid/apierrors"
|
|
preferencesbiz "github.com/tutti-os/tutti/services/tuttid/biz/preferences"
|
|
agentservice "github.com/tutti-os/tutti/services/tuttid/service/agent"
|
|
)
|
|
|
|
type AgentSessionService interface {
|
|
List(context.Context, string) ([]agentservice.Session, error)
|
|
ListFiltered(context.Context, string, agentservice.ListSessionsInput) ([]agentservice.Session, error)
|
|
ListPage(context.Context, string, agentservice.ListSessionsInput) (agentservice.SessionListPage, error)
|
|
ListSessionSections(context.Context, string, agentservice.ListSessionSectionsInput) (agentservice.SessionSectionsPage, error)
|
|
ListSessionSectionPage(context.Context, string, agentservice.ListSessionSectionPageInput) (agentservice.SessionSection, error)
|
|
ListSessionSectionDeletionCandidates(context.Context, string, agentservice.ListSessionSectionDeletionCandidatesInput) (agentservice.SessionSectionDeletionCandidates, error)
|
|
DeleteSessionsBatch(context.Context, string, agentservice.DeleteSessionsBatchInput) (agentservice.DeleteSessionsBatchResult, error)
|
|
ListPinnedSessionPage(context.Context, string, agentservice.ListPinnedSessionPageInput) (agentservice.SessionPage, error)
|
|
GetComposerOptions(context.Context, agentservice.ComposerOptionsInput) (agentservice.ComposerOptions, error)
|
|
ListGeneratedFiles(context.Context, string, agentservice.ListGeneratedFilesInput) (agentservice.GeneratedFileList, error)
|
|
ListMessages(context.Context, string, string, agentservice.ListMessagesInput) (agentservice.SessionMessagesPage, error)
|
|
ScanExternalImports(context.Context, agentservice.ExternalImportScanInput) (agentservice.ExternalImportScanResult, error)
|
|
ImportExternalSessions(context.Context, string, agentservice.ExternalImportInput) (agentservice.ExternalImportResult, error)
|
|
ExternalImportValidProjectPaths(context.Context, agentservice.ExternalImportInput) ([]string, error)
|
|
Create(context.Context, string, agentservice.CreateSessionInput) (agentservice.Session, error)
|
|
Fork(context.Context, string, string, agentservice.ForkSessionInput) (agentservice.SessionForkOperation, error)
|
|
GetSessionForkOperation(context.Context, string, string) (agentservice.SessionForkOperation, error)
|
|
AcknowledgeSessionForkOperation(context.Context, string, string) (agentservice.SessionForkOperation, error)
|
|
Get(context.Context, string, string) (agentservice.Session, error)
|
|
GetDetailWithProjection(context.Context, string, string, agentservice.SessionDetailProjection) (agentservice.SessionDetail, error)
|
|
ReadAttachment(context.Context, string, string, string) (agentservice.PromptAttachment, error)
|
|
ListGitBranches(context.Context, string, string) (agentservice.GitBranches, error)
|
|
ListGitBranchesForPath(context.Context, string, string) (agentservice.GitBranches, error)
|
|
ResolveGitPatchSupportForPath(context.Context, string, string) (agentservice.GitPatchSupport, error)
|
|
ResolveSessionWorktreeSupport(context.Context, string, string, string) (agentservice.SessionWorktreeSupport, error)
|
|
ApplyGitPatchForPath(context.Context, string, agentservice.ApplyGitPatchInput) (agentservice.ApplyGitPatchResult, error)
|
|
Clear(context.Context, string) (agentservice.ClearSessionsResult, error)
|
|
Delete(context.Context, string, string) (agentservice.DeleteSessionResult, error)
|
|
CancelTurn(context.Context, string, string, string) (agentservice.CancelTurnResult, error)
|
|
GoalControl(context.Context, agentservice.GoalControlInput) (agentservice.GoalControlSessionResult, error)
|
|
GetGoalState(context.Context, string, string) (agentservice.GoalStateSessionResult, error)
|
|
ReconcileGoal(context.Context, string, string) (agentservice.GoalStateSessionResult, error)
|
|
SendInput(context.Context, string, string, agentservice.SendInput) (agentservice.SendInputResult, error)
|
|
UpdatePin(context.Context, string, string, bool) (agentservice.Session, error)
|
|
UpdateTitle(context.Context, string, string, string) (agentservice.Session, error)
|
|
UpdateVisible(context.Context, string, string, bool) (agentservice.Session, error)
|
|
UpdateSettings(context.Context, string, string, agentservice.ComposerSettingsPatch) (agentservice.Session, error)
|
|
SubmitInteractive(context.Context, agenthost.InteractionRef, agenthost.SubmitInteractiveInput) (agentservice.Session, error)
|
|
}
|
|
|
|
func (api DaemonAPI) ClearWorkspaceAgentSessions(ctx context.Context, request tuttigenerated.ClearWorkspaceAgentSessionsRequestObject) (tuttigenerated.ClearWorkspaceAgentSessionsResponseObject, error) {
|
|
if api.AgentSessionService == nil {
|
|
return tuttigenerated.ClearWorkspaceAgentSessions503JSONResponse{
|
|
ServiceUnavailableErrorJSONResponse: agentSessionServiceUnavailableError(),
|
|
}, nil
|
|
}
|
|
result, err := api.AgentSessionService.Clear(ctx, string(request.WorkspaceID))
|
|
if err != nil {
|
|
return writeClearWorkspaceAgentSessionsError(err), nil
|
|
}
|
|
return tuttigenerated.ClearWorkspaceAgentSessions200JSONResponse{
|
|
RemovedMessages: result.RemovedMessages,
|
|
RemovedSessions: result.RemovedSessions,
|
|
CleanupFailedSessionIds: append([]string{}, result.CleanupFailedSessionIDs...),
|
|
}, nil
|
|
}
|
|
|
|
func (api DaemonAPI) GetAgentProviderComposerOptions(ctx context.Context, request tuttigenerated.GetAgentProviderComposerOptionsRequestObject) (tuttigenerated.GetAgentProviderComposerOptionsResponseObject, error) {
|
|
if api.AgentSessionService == nil {
|
|
return tuttigenerated.GetAgentProviderComposerOptions503JSONResponse{
|
|
ServiceUnavailableErrorJSONResponse: agentSessionServiceUnavailableError(),
|
|
}, nil
|
|
}
|
|
input := agentservice.ComposerOptionsInput{
|
|
Provider: string(request.Provider),
|
|
}
|
|
if request.Body != nil {
|
|
input.AgentSessionID = optionalStringValue(request.Body.AgentSessionId)
|
|
if request.Body.Section != nil {
|
|
input.Section = agentservice.ComposerOptionsSection(*request.Body.Section)
|
|
}
|
|
input.AgentTargetID = optionalStringValue(request.Body.AgentTargetId)
|
|
input.Cwd = optionalStringValue(request.Body.Cwd)
|
|
input.WorkspaceID = optionalStringValue(request.Body.WorkspaceId)
|
|
input.WaitForFreshModelCatalog = request.Body.WaitForFreshModelCatalog != nil && *request.Body.WaitForFreshModelCatalog
|
|
}
|
|
if request.Body != nil && request.Body.Settings != nil {
|
|
input.Settings = composerSettingsFromGenerated(*request.Body.Settings)
|
|
input.CodexSaverMode = request.Body.Settings.CodexSaverMode
|
|
input.RTKSaverMode = request.Body.Settings.RtkSaverMode
|
|
}
|
|
if request.Body != nil && request.Body.Locale != nil {
|
|
input.Locale = string(*request.Body.Locale)
|
|
} else {
|
|
input.Locale = api.composerDefaultLocale(ctx)
|
|
}
|
|
options, err := api.AgentSessionService.GetComposerOptions(ctx, input)
|
|
if err != nil {
|
|
return writeGetAgentProviderComposerOptionsError(err), nil
|
|
}
|
|
return tuttigenerated.GetAgentProviderComposerOptions200JSONResponse(
|
|
generatedAgentProviderComposerOptions(options),
|
|
), nil
|
|
}
|
|
|
|
func (api DaemonAPI) GetWorkspaceAgentSession(ctx context.Context, request tuttigenerated.GetWorkspaceAgentSessionRequestObject) (tuttigenerated.GetWorkspaceAgentSessionResponseObject, error) {
|
|
if request.Params.Projection != nil && !request.Params.Projection.Valid() {
|
|
return tuttigenerated.GetWorkspaceAgentSession400JSONResponse{
|
|
InvalidRequestErrorJSONResponse: invalidRequestError(
|
|
apierrors.InvalidRequest(
|
|
apierrors.ReasonMalformedRequest,
|
|
apierrors.WithDeveloperMessage("unsupported agent session detail projection"),
|
|
),
|
|
),
|
|
}, nil
|
|
}
|
|
if api.AgentSessionService == nil {
|
|
return tuttigenerated.GetWorkspaceAgentSession503JSONResponse{
|
|
ServiceUnavailableErrorJSONResponse: agentSessionServiceUnavailableError(),
|
|
}, nil
|
|
}
|
|
projection := agentservice.SessionDetailProjectionFull
|
|
if request.Params.Projection != nil &&
|
|
*request.Params.Projection == tuttigenerated.MessageHydration {
|
|
projection = agentservice.SessionDetailProjectionMessageHydration
|
|
}
|
|
detail, err := api.AgentSessionService.GetDetailWithProjection(
|
|
ctx,
|
|
string(request.WorkspaceID),
|
|
string(request.AgentSessionID),
|
|
projection,
|
|
)
|
|
if err != nil {
|
|
return writeGetWorkspaceAgentSessionError(err), nil
|
|
}
|
|
generatedSession, err := generatedAgentSession(detail.Session)
|
|
if err != nil {
|
|
return writeGetWorkspaceAgentSessionError(err), nil
|
|
}
|
|
generatedChildren, err := generatedAgentSessions(detail.ChildSessions)
|
|
if err != nil {
|
|
return writeGetWorkspaceAgentSessionError(err), nil
|
|
}
|
|
return tuttigenerated.GetWorkspaceAgentSession200JSONResponse{
|
|
Session: generatedSession,
|
|
ChildSessions: generatedChildren,
|
|
Turns: generatedAgentTurns(detail.Turns),
|
|
Projection: tuttigenerated.WorkspaceAgentSessionDetailProjection(projection),
|
|
LifecycleCapabilitiesProjected: projection == agentservice.SessionDetailProjectionFull,
|
|
EditRetry: generatedAgentEditRetryAvailability(detail.EditRetry),
|
|
}, nil
|
|
}
|
|
|
|
func generatedAgentTurns(turns []agentactivitybiz.Turn) []tuttigenerated.WorkspaceAgentTurn {
|
|
result := make([]tuttigenerated.WorkspaceAgentTurn, 0, len(turns))
|
|
for _, turn := range turns {
|
|
result = append(result, agentservice.GeneratedWorkspaceAgentTurn(turn))
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (api DaemonAPI) DeleteWorkspaceAgentSession(ctx context.Context, request tuttigenerated.DeleteWorkspaceAgentSessionRequestObject) (tuttigenerated.DeleteWorkspaceAgentSessionResponseObject, error) {
|
|
if api.AgentSessionService == nil {
|
|
return tuttigenerated.DeleteWorkspaceAgentSession503JSONResponse{
|
|
ServiceUnavailableErrorJSONResponse: agentSessionServiceUnavailableError(),
|
|
}, nil
|
|
}
|
|
result, err := api.AgentSessionService.Delete(ctx, string(request.WorkspaceID), string(request.AgentSessionID))
|
|
if err != nil {
|
|
return writeDeleteWorkspaceAgentSessionError(err), nil
|
|
}
|
|
return tuttigenerated.DeleteWorkspaceAgentSession200JSONResponse{
|
|
Removed: result.Removed, CleanupFailed: result.CleanupFailed,
|
|
}, nil
|
|
}
|
|
|
|
func (api DaemonAPI) ListWorkspaceAgentSessionMessages(ctx context.Context, request tuttigenerated.ListWorkspaceAgentSessionMessagesRequestObject) (tuttigenerated.ListWorkspaceAgentSessionMessagesResponseObject, error) {
|
|
if api.AgentSessionService == nil {
|
|
return tuttigenerated.ListWorkspaceAgentSessionMessages503JSONResponse{
|
|
ServiceUnavailableErrorJSONResponse: agentSessionServiceUnavailableError(),
|
|
}, nil
|
|
}
|
|
startedAt := time.Now()
|
|
workspaceID := string(request.WorkspaceID)
|
|
agentSessionID := string(request.AgentSessionID)
|
|
input := agentservice.ListMessagesInput{}
|
|
if request.Params.AfterVersion != nil {
|
|
afterVersion, err := workspaceAgentMessageCursorFromRequest(*request.Params.AfterVersion)
|
|
if err != nil {
|
|
return writeListWorkspaceAgentSessionMessagesError(agentservice.ErrInvalidArgument), nil
|
|
}
|
|
input.AfterVersion = afterVersion
|
|
}
|
|
if request.Params.BeforeVersion != nil {
|
|
beforeVersion, err := workspaceAgentMessageCursorFromRequest(*request.Params.BeforeVersion)
|
|
if err != nil {
|
|
return writeListWorkspaceAgentSessionMessagesError(agentservice.ErrInvalidArgument), nil
|
|
}
|
|
input.BeforeVersion = beforeVersion
|
|
}
|
|
if request.Params.Order != nil {
|
|
switch *request.Params.Order {
|
|
case tuttigenerated.Asc:
|
|
input.Order = agentactivitybiz.MessageOrderAsc
|
|
case tuttigenerated.Desc:
|
|
input.Order = agentactivitybiz.MessageOrderDesc
|
|
default:
|
|
return writeListWorkspaceAgentSessionMessagesError(agentservice.ErrInvalidArgument), nil
|
|
}
|
|
}
|
|
if request.Params.Limit != nil {
|
|
if *request.Params.Limit <= 0 {
|
|
return writeListWorkspaceAgentSessionMessagesError(agentservice.ErrInvalidArgument), nil
|
|
}
|
|
input.Limit = *request.Params.Limit
|
|
}
|
|
slog.Debug("workspace agent session messages list requested",
|
|
"event", "workspace.agent_session.messages.api.list_requested",
|
|
"workspace_id", workspaceID,
|
|
"agent_session_id", agentSessionID,
|
|
"after_version", input.AfterVersion,
|
|
"before_version", input.BeforeVersion,
|
|
"order", input.Order,
|
|
"limit", input.Limit,
|
|
)
|
|
page, err := api.AgentSessionService.ListMessages(
|
|
ctx,
|
|
workspaceID,
|
|
agentSessionID,
|
|
input,
|
|
)
|
|
if err != nil {
|
|
slog.Warn("workspace agent session messages list failed",
|
|
"event", "workspace.agent_session.messages.api.list_failed",
|
|
"workspace_id", workspaceID,
|
|
"agent_session_id", agentSessionID,
|
|
"after_version", input.AfterVersion,
|
|
"before_version", input.BeforeVersion,
|
|
"order", input.Order,
|
|
"limit", input.Limit,
|
|
"duration_ms", time.Since(startedAt).Milliseconds(),
|
|
"error", err,
|
|
)
|
|
return writeListWorkspaceAgentSessionMessagesError(err), nil
|
|
}
|
|
messages, err := generatedAgentSessionMessages(page.Messages)
|
|
var latestVersion int64
|
|
if err == nil {
|
|
latestVersion, err = generatedWorkspaceAgentSafeInteger("latest message version", page.LatestVersion)
|
|
}
|
|
if err != nil {
|
|
firstVersion, lastVersion := agentSessionMessageVersionRange(page.Messages)
|
|
slog.Warn("workspace agent session messages response transform failed",
|
|
"event", "workspace.agent_session.messages.api.transform_failed",
|
|
"workspace_id", workspaceID,
|
|
"agent_session_id", agentSessionID,
|
|
"after_version", input.AfterVersion,
|
|
"before_version", input.BeforeVersion,
|
|
"order", input.Order,
|
|
"limit", input.Limit,
|
|
"message_count", len(page.Messages),
|
|
"first_version", firstVersion,
|
|
"last_version", lastVersion,
|
|
"latest_version", page.LatestVersion,
|
|
"has_more", page.HasMore,
|
|
"duration_ms", time.Since(startedAt).Milliseconds(),
|
|
"error", err,
|
|
)
|
|
return writeListWorkspaceAgentSessionMessagesError(err), nil
|
|
}
|
|
firstVersion, lastVersion := generatedAgentSessionMessageVersionRange(messages)
|
|
slog.Debug("workspace agent session messages list completed",
|
|
"event", "workspace.agent_session.messages.api.list_completed",
|
|
"workspace_id", workspaceID,
|
|
"agent_session_id", agentSessionID,
|
|
"after_version", input.AfterVersion,
|
|
"before_version", input.BeforeVersion,
|
|
"order", input.Order,
|
|
"limit", input.Limit,
|
|
"message_count", len(messages),
|
|
"first_version", firstVersion,
|
|
"last_version", lastVersion,
|
|
"latest_version", page.LatestVersion,
|
|
"has_more", page.HasMore,
|
|
"duration_ms", time.Since(startedAt).Milliseconds(),
|
|
)
|
|
return tuttigenerated.ListWorkspaceAgentSessionMessages200JSONResponse{
|
|
AgentSessionId: page.AgentSessionID,
|
|
HasMore: page.HasMore,
|
|
LatestVersion: latestVersion,
|
|
Messages: messages,
|
|
}, nil
|
|
}
|
|
|
|
func (api DaemonAPI) ListWorkspaceAgentGeneratedFiles(ctx context.Context, request tuttigenerated.ListWorkspaceAgentGeneratedFilesRequestObject) (tuttigenerated.ListWorkspaceAgentGeneratedFilesResponseObject, error) {
|
|
if api.AgentSessionService == nil {
|
|
return tuttigenerated.ListWorkspaceAgentGeneratedFiles503JSONResponse{
|
|
ServiceUnavailableErrorJSONResponse: agentSessionServiceUnavailableError(),
|
|
}, nil
|
|
}
|
|
input := agentservice.ListGeneratedFilesInput{}
|
|
input.SectionKey = strings.TrimSpace(request.Params.SectionKey)
|
|
if request.Params.Query != nil {
|
|
input.Query = strings.TrimSpace(*request.Params.Query)
|
|
}
|
|
if request.Params.AgentTargetIds != nil {
|
|
if len(*request.Params.AgentTargetIds) > agentservice.MaxGeneratedFileAgentTargetFilters {
|
|
return writeListWorkspaceAgentGeneratedFilesError(agentservice.ErrInvalidArgument), nil
|
|
}
|
|
input.AgentTargetIDs = append([]string(nil), (*request.Params.AgentTargetIds)...)
|
|
}
|
|
if request.Params.Cursor != nil {
|
|
input.Cursor = strings.TrimSpace(*request.Params.Cursor)
|
|
}
|
|
if request.Params.Limit != nil {
|
|
if *request.Params.Limit <= 0 || *request.Params.Limit > 100 {
|
|
return writeListWorkspaceAgentGeneratedFilesError(agentservice.ErrInvalidArgument), nil
|
|
}
|
|
input.Limit = *request.Params.Limit
|
|
}
|
|
result, err := api.AgentSessionService.ListGeneratedFiles(
|
|
ctx,
|
|
string(request.WorkspaceID),
|
|
input,
|
|
)
|
|
if err != nil {
|
|
return writeListWorkspaceAgentGeneratedFilesError(err), nil
|
|
}
|
|
response := tuttigenerated.ListWorkspaceAgentGeneratedFiles200JSONResponse{
|
|
Entries: generatedAgentGeneratedFiles(result.Files),
|
|
HasMore: result.HasMore,
|
|
WorkspaceId: result.WorkspaceID,
|
|
}
|
|
if result.NextCursor != "" {
|
|
response.NextCursor = &result.NextCursor
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
func (api DaemonAPI) ReadWorkspaceAgentSessionAttachment(ctx context.Context, request tuttigenerated.ReadWorkspaceAgentSessionAttachmentRequestObject) (tuttigenerated.ReadWorkspaceAgentSessionAttachmentResponseObject, error) {
|
|
if api.AgentSessionService == nil {
|
|
return tuttigenerated.ReadWorkspaceAgentSessionAttachment503JSONResponse{
|
|
ServiceUnavailableErrorJSONResponse: agentSessionServiceUnavailableError(),
|
|
}, nil
|
|
}
|
|
attachment, err := api.AgentSessionService.ReadAttachment(
|
|
ctx,
|
|
string(request.WorkspaceID),
|
|
string(request.AgentSessionID),
|
|
string(request.AttachmentID),
|
|
)
|
|
if err != nil {
|
|
return writeReadWorkspaceAgentSessionAttachmentError(err), nil
|
|
}
|
|
return tuttigenerated.ReadWorkspaceAgentSessionAttachment200JSONResponse{
|
|
AttachmentId: attachment.AttachmentID,
|
|
MimeType: tuttigenerated.WorkspaceAgentSessionAttachmentResponseMimeType(attachment.MimeType),
|
|
Data: attachment.Data,
|
|
}, nil
|
|
}
|
|
|
|
func (api DaemonAPI) ListWorkspaceAgentSessionGitBranches(ctx context.Context, request tuttigenerated.ListWorkspaceAgentSessionGitBranchesRequestObject) (tuttigenerated.ListWorkspaceAgentSessionGitBranchesResponseObject, error) {
|
|
if api.AgentSessionService == nil {
|
|
return tuttigenerated.ListWorkspaceAgentSessionGitBranches503JSONResponse{
|
|
ServiceUnavailableErrorJSONResponse: agentSessionServiceUnavailableError(),
|
|
}, nil
|
|
}
|
|
branches, err := api.AgentSessionService.ListGitBranches(ctx, string(request.WorkspaceID), string(request.AgentSessionID))
|
|
if err != nil {
|
|
return writeListWorkspaceAgentSessionGitBranchesError(err), nil
|
|
}
|
|
response := tuttigenerated.ListWorkspaceAgentSessionGitBranches200JSONResponse{Branches: branches.Branches}
|
|
if response.Branches == nil {
|
|
response.Branches = []string{}
|
|
}
|
|
if branches.CurrentBranch != "" {
|
|
current := branches.CurrentBranch
|
|
response.CurrentBranch = ¤t
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
func (api DaemonAPI) ListWorkspaceGitBranches(ctx context.Context, request tuttigenerated.ListWorkspaceGitBranchesRequestObject) (tuttigenerated.ListWorkspaceGitBranchesResponseObject, error) {
|
|
if api.AgentSessionService == nil {
|
|
return tuttigenerated.ListWorkspaceGitBranches503JSONResponse{
|
|
ServiceUnavailableErrorJSONResponse: agentSessionServiceUnavailableError(),
|
|
}, nil
|
|
}
|
|
branches, err := api.AgentSessionService.ListGitBranchesForPath(ctx, string(request.WorkspaceID), request.Params.WorkingDirectory)
|
|
if err != nil {
|
|
return writeListWorkspaceGitBranchesError(err), nil
|
|
}
|
|
response := tuttigenerated.ListWorkspaceGitBranches200JSONResponse{Branches: branches.Branches}
|
|
if response.Branches == nil {
|
|
response.Branches = []string{}
|
|
}
|
|
if branches.CurrentBranch != "" {
|
|
current := branches.CurrentBranch
|
|
response.CurrentBranch = ¤t
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
func (api DaemonAPI) SubmitWorkspaceAgentInteractive(ctx context.Context, request tuttigenerated.SubmitWorkspaceAgentInteractiveRequestObject) (tuttigenerated.SubmitWorkspaceAgentInteractiveResponseObject, error) {
|
|
if api.AgentSessionService == nil {
|
|
return tuttigenerated.SubmitWorkspaceAgentInteractive503JSONResponse{
|
|
ServiceUnavailableErrorJSONResponse: agentSessionServiceUnavailableError(),
|
|
}, nil
|
|
}
|
|
if request.Body == nil {
|
|
return tuttigenerated.SubmitWorkspaceAgentInteractive400JSONResponse{
|
|
InvalidRequestErrorJSONResponse: invalidRequestError(apierrors.EmptyBody(apierrors.WithDeveloperMessage("empty body"))),
|
|
}, nil
|
|
}
|
|
session, err := api.AgentSessionService.SubmitInteractive(ctx, agenthost.InteractionRef{
|
|
WorkspaceID: string(request.WorkspaceID), AgentSessionID: string(request.AgentSessionID),
|
|
TurnID: request.Body.TurnId, RequestID: string(request.RequestID),
|
|
}, agenthost.SubmitInteractiveInput{
|
|
Action: request.Body.Action,
|
|
OptionID: request.Body.OptionId,
|
|
Payload: optionalPayloadMap(request.Body.Payload),
|
|
})
|
|
if err != nil {
|
|
return writeSubmitWorkspaceAgentInteractiveError(err), nil
|
|
}
|
|
generatedSession, err := generatedAgentSession(session)
|
|
if err != nil {
|
|
return writeSubmitWorkspaceAgentInteractiveError(err), nil
|
|
}
|
|
if !isRendererEngineCommandOrigin(
|
|
request.Params.XTuttiAgentCommandOrigin,
|
|
) {
|
|
api.recordAgentStimulus(ctx, "interactive.response", string(request.WorkspaceID), string(request.AgentSessionID), map[string]any{
|
|
"turnId": request.Body.TurnId,
|
|
"requestId": string(request.RequestID),
|
|
"action": request.Body.Action,
|
|
"optionId": request.Body.OptionId,
|
|
"payload": request.Body.Payload,
|
|
})
|
|
}
|
|
return tuttigenerated.SubmitWorkspaceAgentInteractive200JSONResponse{
|
|
Session: generatedSession,
|
|
}, nil
|
|
}
|
|
|
|
func generatedAgentSessions(sessions []agentservice.Session) ([]tuttigenerated.WorkspaceAgentSession, error) {
|
|
result := make([]tuttigenerated.WorkspaceAgentSession, 0, len(sessions))
|
|
for _, session := range sessions {
|
|
generated, err := generatedAgentSession(session)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, generated)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func generatedAgentSessionPage(page agentservice.SessionPage) (tuttigenerated.WorkspaceAgentSessionPage, error) {
|
|
sessions, err := generatedAgentSessions(page.Sessions)
|
|
if err != nil {
|
|
return tuttigenerated.WorkspaceAgentSessionPage{}, err
|
|
}
|
|
response := tuttigenerated.WorkspaceAgentSessionPage{
|
|
HasMore: page.HasMore,
|
|
Sessions: sessions,
|
|
TotalCount: page.TotalCount,
|
|
}
|
|
if strings.TrimSpace(page.NextCursor) != "" {
|
|
response.NextCursor = &page.NextCursor
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
func generatedAgentSessionSections(sections []agentservice.SessionSection) ([]tuttigenerated.WorkspaceAgentSessionSection, error) {
|
|
result := make([]tuttigenerated.WorkspaceAgentSessionSection, 0, len(sections))
|
|
for _, section := range sections {
|
|
generated, err := generatedAgentSessionSection(section)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, generated)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func generatedAgentSessionSection(section agentservice.SessionSection) (tuttigenerated.WorkspaceAgentSessionSection, error) {
|
|
var userProject *tuttigenerated.UserProject
|
|
if section.UserProject != nil {
|
|
value := generatedUserProject(*section.UserProject)
|
|
userProject = &value
|
|
}
|
|
sessions, err := generatedAgentSessions(section.Sessions)
|
|
if err != nil {
|
|
return tuttigenerated.WorkspaceAgentSessionSection{}, err
|
|
}
|
|
response := tuttigenerated.WorkspaceAgentSessionSection{
|
|
HasMore: section.HasMore,
|
|
Kind: tuttigenerated.WorkspaceAgentSessionSectionKind(section.Kind),
|
|
SectionKey: section.SectionKey,
|
|
Sessions: sessions,
|
|
TotalCount: section.TotalCount,
|
|
UserProject: userProject,
|
|
}
|
|
if strings.TrimSpace(section.NextCursor) != "" {
|
|
response.NextCursor = §ion.NextCursor
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
func composerSettingsFromGenerated(settings tuttigenerated.AgentSessionComposerSettings) agentservice.ComposerSettings {
|
|
return agentservice.ComposerSettings{
|
|
CodexSaverMode: settings.CodexSaverMode != nil && *settings.CodexSaverMode,
|
|
RTKSaverMode: settings.RtkSaverMode != nil && *settings.RtkSaverMode,
|
|
Model: optionalStringValue(settings.Model),
|
|
PermissionModeID: optionalStringValue(settings.PermissionModeId),
|
|
PlanMode: settings.PlanMode != nil && *settings.PlanMode,
|
|
BrowserUse: settings.BrowserUse,
|
|
ReasoningEffort: optionalStringValue(settings.ReasoningEffort),
|
|
Speed: optionalStringValue(settings.Speed),
|
|
}
|
|
}
|
|
|
|
func (api DaemonAPI) composerDefaultLocale(ctx context.Context) string {
|
|
if api.PreferencesService == nil {
|
|
return ""
|
|
}
|
|
preferences, err := api.PreferencesService.Get(ctx)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return preferences.Locale
|
|
}
|
|
|
|
func (api DaemonAPI) agentConversationDetailMode(ctx context.Context) string {
|
|
if api.PreferencesService == nil {
|
|
return preferencesbiz.DefaultDesktopAgentConversationDetailMode
|
|
}
|
|
preferences, err := api.PreferencesService.Get(ctx)
|
|
if err != nil {
|
|
return preferencesbiz.DefaultDesktopAgentConversationDetailMode
|
|
}
|
|
return preferencesbiz.NormalizeDesktopAgentConversationDetailMode(preferences.AgentConversationDetailMode)
|
|
}
|
|
|
|
func composerSettingsPatchFromGenerated(settings tuttigenerated.AgentSessionComposerSettings) agentservice.ComposerSettingsPatch {
|
|
return agentservice.ComposerSettingsPatch{
|
|
CodexSaverMode: settings.CodexSaverMode,
|
|
RTKSaverMode: settings.RtkSaverMode,
|
|
Model: settings.Model,
|
|
PermissionModeID: settings.PermissionModeId,
|
|
PlanMode: settings.PlanMode,
|
|
BrowserUse: settings.BrowserUse,
|
|
ReasoningEffort: settings.ReasoningEffort,
|
|
Speed: settings.Speed,
|
|
}
|
|
}
|
|
|
|
func generatedAgentProviderComposerOptions(options agentservice.ComposerOptions) tuttigenerated.AgentProviderComposerOptionsResponse {
|
|
effectiveSettings := generatedAgentSessionComposerSettings(options.EffectiveSettings)
|
|
return tuttigenerated.AgentProviderComposerOptionsResponse{
|
|
CodexSaverModeSupported: &options.CodexSaverModeSupported,
|
|
RtkSaverModeSupported: &options.RTKSaverModeSupported,
|
|
Behavior: tuttigenerated.AgentProviderComposerBehavior{
|
|
CollapseModelOptionsToLatest: options.Behavior.CollapseModelOptionsToLatest,
|
|
ModelOptionsAuthoritative: options.Behavior.ModelOptionsAuthoritative,
|
|
RefreshModelOptionsAfterSettings: options.Behavior.RefreshModelOptionsAfterSettings,
|
|
PrewarmDraftSession: options.Behavior.PrewarmDraftSession,
|
|
PlanModeExclusiveWithPermissionMode: options.Behavior.PlanModeExclusiveWithPermissionMode,
|
|
},
|
|
Capabilities: generatedAgentSessionCapabilities(canonical.NewCapabilitySnapshot(options.Capabilities)),
|
|
CapabilityCatalog: generatedAgentProviderCapabilityOptions(options.CapabilityCatalog),
|
|
Commands: generatedAgentProviderComposerCommands(options.Commands),
|
|
EffectiveSettings: effectiveSettings,
|
|
ModelConfig: generatedComposerConfigOption(options.ModelConfig),
|
|
PermissionConfig: generatedPermissionConfig(options.PermissionConfig),
|
|
Provider: tuttigenerated.WorkspaceAgentProvider(options.Provider),
|
|
ReasoningConfig: generatedComposerConfigOption(options.ReasoningConfig),
|
|
ReasoningOptionsByModel: generatedAgentProviderComposerReasoningOptionsByModel(
|
|
options.ReasoningOptionsByModel,
|
|
),
|
|
SpeedConfig: generatedComposerConfigOptionPointer(options.SpeedConfig),
|
|
RuntimeContext: options.RuntimeContext,
|
|
Skills: generatedAgentProviderSkillOptions(options.Skills),
|
|
SlashCommandPolicy: generatedAgentSlashCommandPolicy(options.SlashCommandPolicy),
|
|
}
|
|
}
|
|
|
|
func generatedAgentSlashCommandPolicy(
|
|
policy *providerregistry.SlashCommandPolicyDescriptor,
|
|
) *tuttigenerated.AgentSlashCommandPolicy {
|
|
if policy == nil {
|
|
return nil
|
|
}
|
|
effects := make([]tuttigenerated.AgentSlashCommandEffectDescriptor, 0, len(policy.CommandEffects))
|
|
for _, effect := range policy.CommandEffects {
|
|
effects = append(effects, tuttigenerated.AgentSlashCommandEffectDescriptor{
|
|
Command: strings.TrimSpace(effect.Command),
|
|
Effect: tuttigenerated.AgentSlashCommandEffect(effect.Effect),
|
|
})
|
|
}
|
|
return &tuttigenerated.AgentSlashCommandPolicy{
|
|
FallbackCommands: append(make([]string, 0, len(policy.FallbackCommands)), policy.FallbackCommands...),
|
|
CommandEffects: effects,
|
|
CommandCatalogAuthoritative: boolPointer(policy.CommandCatalogAuthoritative),
|
|
}
|
|
}
|
|
|
|
func generatedAgentSessionComposerSettings(settings agentservice.ComposerSettings) tuttigenerated.AgentSessionComposerSettings {
|
|
result := tuttigenerated.AgentSessionComposerSettings{
|
|
CodexSaverMode: boolPointer(settings.CodexSaverMode),
|
|
RtkSaverMode: boolPointer(settings.RTKSaverMode),
|
|
Model: optionalStringPointer(strings.TrimSpace(settings.Model)),
|
|
PermissionModeId: optionalStringPointer(strings.TrimSpace(settings.PermissionModeID)),
|
|
PlanMode: boolPointer(settings.PlanMode),
|
|
ReasoningEffort: optionalStringPointer(strings.TrimSpace(settings.ReasoningEffort)),
|
|
Speed: optionalStringPointer(strings.TrimSpace(settings.Speed)),
|
|
}
|
|
if settings.BrowserUse != nil {
|
|
result.BrowserUse = settings.BrowserUse
|
|
}
|
|
return result
|
|
}
|
|
|
|
func generatedPermissionConfig(config agentservice.PermissionConfig) tuttigenerated.PermissionConfig {
|
|
result := tuttigenerated.PermissionConfig{
|
|
Configurable: config.Configurable,
|
|
Modes: make([]tuttigenerated.PermissionModeOption, 0, len(config.Modes)),
|
|
}
|
|
if strings.TrimSpace(config.DefaultValue) != "" {
|
|
result.DefaultValue = optionalStringPointer(config.DefaultValue)
|
|
}
|
|
for _, mode := range config.Modes {
|
|
option := tuttigenerated.PermissionModeOption{
|
|
Id: strings.TrimSpace(mode.ID),
|
|
Label: strings.TrimSpace(mode.Label),
|
|
Semantic: tuttigenerated.PermissionModeSemantic(mode.Semantic),
|
|
}
|
|
if strings.TrimSpace(mode.Description) != "" {
|
|
option.Description = optionalStringPointer(mode.Description)
|
|
}
|
|
if option.Id != "" && option.Label != "" {
|
|
result.Modes = append(result.Modes, option)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func optionalStringValue(input *string) string {
|
|
if input == nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(*input)
|
|
}
|
|
|
|
func optionalPayloadMap(input *map[string]interface{}) map[string]any {
|
|
if input == nil {
|
|
return nil
|
|
}
|
|
return map[string]any(*input)
|
|
}
|
|
|
|
func optionalStringPointer(value string) *string {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return nil
|
|
}
|
|
return &value
|
|
}
|
|
|
|
func generatedAgentGeneratedFiles(files []agentservice.GeneratedFile) []tuttigenerated.WorkspaceAgentGeneratedFileEntry {
|
|
result := make([]tuttigenerated.WorkspaceAgentGeneratedFileEntry, 0, len(files))
|
|
for _, file := range files {
|
|
path := strings.TrimSpace(file.Path)
|
|
if path == "" {
|
|
continue
|
|
}
|
|
label := strings.TrimSpace(file.Label)
|
|
if label == "" {
|
|
label = path
|
|
}
|
|
result = append(result, tuttigenerated.WorkspaceAgentGeneratedFileEntry{
|
|
Label: label,
|
|
Path: path,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
func generatedAgentSession(session agentservice.Session) (tuttigenerated.WorkspaceAgentSession, error) {
|
|
var settings *tuttigenerated.AgentSessionComposerSettings
|
|
if session.Settings != nil {
|
|
value := generatedAgentSessionComposerSettings(*session.Settings)
|
|
settings = &value
|
|
}
|
|
// Protocol v2 turn state: the session carries an activeTurnId reference
|
|
// plus the embedded active turn and pending interactions.
|
|
var activeTurn *tuttigenerated.WorkspaceAgentTurn
|
|
if session.ActiveTurn != nil {
|
|
turn := generatedWorkspaceAgentTurn(*session.ActiveTurn)
|
|
activeTurn = &turn
|
|
}
|
|
var latestTurn *tuttigenerated.WorkspaceAgentTurn
|
|
if session.LatestTurn != nil {
|
|
turn := generatedWorkspaceAgentTurn(*session.LatestTurn)
|
|
latestTurn = &turn
|
|
}
|
|
pendingInteractions := make([]tuttigenerated.WorkspaceAgentInteraction, 0, len(session.PendingInteractions))
|
|
for _, interaction := range session.PendingInteractions {
|
|
pendingInteractions = append(pendingInteractions, generatedWorkspaceAgentInteraction(interaction))
|
|
}
|
|
latestTurnInteractions := make([]tuttigenerated.WorkspaceAgentInteraction, 0, len(session.LatestTurnInteractions))
|
|
for _, interaction := range session.LatestTurnInteractions {
|
|
latestTurnInteractions = append(latestTurnInteractions, generatedWorkspaceAgentInteraction(interaction))
|
|
}
|
|
updatedAtUnixMS := session.CreatedAt.UnixMilli()
|
|
if session.UpdatedAt != nil {
|
|
updatedAtUnixMS = session.UpdatedAt.UnixMilli()
|
|
}
|
|
var endedAtUnixMS *int64
|
|
if session.EndedAt != nil {
|
|
value := session.EndedAt.UnixMilli()
|
|
endedAtUnixMS = &value
|
|
}
|
|
generatedSettings := tuttigenerated.AgentSessionComposerSettings{}
|
|
if settings != nil {
|
|
generatedSettings = *settings
|
|
}
|
|
tuttiModeActivation, err := generatedTuttiModeActivation(session.TuttiModeActivation)
|
|
if err != nil {
|
|
return tuttigenerated.WorkspaceAgentSession{}, err
|
|
}
|
|
messageVersion, err := generatedWorkspaceAgentSafeInteger("session message version", session.MessageVersion)
|
|
if err != nil {
|
|
return tuttigenerated.WorkspaceAgentSession{}, fmt.Errorf("project workspace agent session %q: %w", session.ID, err)
|
|
}
|
|
var forkedFrom *tuttigenerated.WorkspaceAgentSessionForkLineage
|
|
if session.ForkedFrom != nil {
|
|
forkedFrom = &tuttigenerated.WorkspaceAgentSessionForkLineage{
|
|
ForkedAtUnixMs: session.ForkedFrom.ForkedAtUnixMS,
|
|
OperationId: strings.TrimSpace(session.ForkedFrom.OperationID),
|
|
SourceAgentSessionId: strings.TrimSpace(session.ForkedFrom.SourceAgentSessionID),
|
|
SourceTurnId: strings.TrimSpace(session.ForkedFrom.SourceTurnID),
|
|
TargetTurnId: strings.TrimSpace(session.ForkedFrom.TargetTurnID),
|
|
}
|
|
}
|
|
goalSyncState := generatedAgentSessionGoalSyncState(session.GoalSyncState)
|
|
return tuttigenerated.WorkspaceAgentSession{
|
|
ActiveTurn: activeTurn,
|
|
ActiveTurnId: optionalStringPointer(strings.TrimSpace(session.ActiveTurnID)),
|
|
AgentTargetId: optionalStringPointer(strings.TrimSpace(session.AgentTargetID)),
|
|
Capabilities: generatedAgentSessionCapabilities(session.Capabilities),
|
|
CreatedAtUnixMs: session.CreatedAt.UnixMilli(),
|
|
Cwd: stringPointer(strings.TrimSpace(session.Cwd)),
|
|
EndedAtUnixMs: endedAtUnixMS,
|
|
ForkedFrom: forkedFrom,
|
|
Goal: generatedAgentSessionGoal(session.Metadata.Goal),
|
|
GoalSyncState: goalSyncState,
|
|
Id: session.ID,
|
|
Imported: session.Metadata.Imported,
|
|
Isolation: generatedAgentSessionIsolation(session.Isolation),
|
|
Kind: tuttigenerated.WorkspaceAgentSessionKind(session.Kind),
|
|
LatestTurn: latestTurn,
|
|
LatestTurnInteractions: latestTurnInteractions,
|
|
MessageVersion: messageVersion,
|
|
LifecycleCapabilities: tuttigenerated.WorkspaceAgentSessionLifecycleCapabilities{
|
|
Fork: session.LifecycleCapabilities.Fork,
|
|
ForkThroughTurn: session.LifecycleCapabilities.ForkThroughTurn,
|
|
},
|
|
ParentAgentSessionId: optionalStringPointer(strings.TrimSpace(session.ParentAgentSessionID)),
|
|
ParentToolCallId: optionalStringPointer(strings.TrimSpace(session.ParentToolCallID)),
|
|
ParentTurnId: optionalStringPointer(strings.TrimSpace(session.ParentTurnID)),
|
|
PendingInteractions: pendingInteractions,
|
|
PermissionConfig: generatedPermissionConfig(session.PermissionConfig),
|
|
Provider: tuttigenerated.WorkspaceAgentProvider(session.Provider),
|
|
ProviderSessionId: stringPointer(strings.TrimSpace(session.ProviderSessionID)),
|
|
PinnedAtUnixMs: int64Pointer(session.PinnedAtUnixMS),
|
|
RailSectionKey: strings.TrimSpace(session.RailSectionKey),
|
|
Resumable: session.Resumable,
|
|
RootAgentSessionId: optionalStringPointer(strings.TrimSpace(session.RootAgentSessionID)),
|
|
RootTurnId: optionalStringPointer(strings.TrimSpace(session.RootTurnID)),
|
|
Settings: generatedSettings,
|
|
Title: session.Title,
|
|
TuttiModeActivation: tuttiModeActivation,
|
|
UpdatedAtUnixMs: updatedAtUnixMS,
|
|
Usage: generatedAgentSessionUsage(session.Metadata.Usage),
|
|
Visible: session.Visible,
|
|
}, nil
|
|
}
|
|
|
|
func int64Pointer(value int64) *int64 {
|
|
if value == 0 {
|
|
return nil
|
|
}
|
|
return &value
|
|
}
|