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 }